diff --git a/.eslintrc b/.eslintrc index 71e21b4381..bfbe34ea65 100644 --- a/.eslintrc +++ b/.eslintrc @@ -42,12 +42,12 @@ "invisible":true, "asyncRender":true, "selectDropdown":true, - "selectBox":true, - "expandSelectBox":true, - "collapseSelectBox":true, - "selectBoxSelectRow":true, - "selectBoxSelectNoneRow":true, - "selectBoxFillInFilter":true, + "selectKit":true, + "expandSelectKit":true, + "collapseSelectKit":true, + "selectKitSelectRow":true, + "selectKitSelectNoneRow":true, + "selectKitFillInFilter":true, "asyncTestDiscourse":true, "fixture":true, "find":true, diff --git a/.overcommit.yml b/.overcommit.yml new file mode 100644 index 0000000000..2de35d6df9 --- /dev/null +++ b/.overcommit.yml @@ -0,0 +1,23 @@ +# Use this file to configure the Overcommit hooks you wish to use. This will +# extend the default configuration defined in: +# https://github.com/brigade/overcommit/blob/master/config/default.yml +# +# At the topmost level of this YAML file is a key representing type of hook +# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can +# customize each hook, such as whether to only run it on certain files (via +# `include`), whether to only display output if it fails (via `quiet`), etc. +# +# For a complete list of hooks, see: +# https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook +# +# For a complete list of options that you can use to customize hooks, see: +# https://github.com/brigade/overcommit#configuration + +PreCommit: + RuboCop: + enabled: true + command: ['bundle', 'exec', 'rubocop'] + EsLint: + enabled: true + command: ['eslint', '--ext', '.es6', '-f', 'compact'] + include: '**/*.es6' diff --git a/.rubocop.yml b/.rubocop.yml index a529980180..b897ad3b9e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ AllCops: - 'bundle/**/*' - 'vendor/**/*' - 'node_modules/**/*' + - 'public/**/*' # Prefer &&/|| over and/or. Style/AndOr: diff --git a/Gemfile b/Gemfile index befe82d775..93d7138d16 100644 --- a/Gemfile +++ b/Gemfile @@ -22,12 +22,13 @@ else gem 'activesupport', '~> 5.1' gem 'railties', '~> 5.1' gem 'sprockets-rails' - gem 'seed-fu', '~> 2.3.5' + gem 'seed-fu' end gem 'mail' gem 'mime-types', require: 'mime/types/columnar' gem 'mini_mime' +gem 'mini_suffix' gem 'hiredis' gem 'redis', require: ["redis", "redis/connection/hiredis"] @@ -35,7 +36,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.19' +gem 'onebox', '1.8.28' gem 'http_accept_language', '~>2.0.5', require: false @@ -46,7 +47,7 @@ gem 'barber' gem 'message_bus' -gem 'rails_multisite', '~> 1.1.0.rc4' +gem 'rails_multisite' gem 'fast_xs' @@ -173,6 +174,7 @@ gem 'memory_profiler', require: false, platform: :mri gem 'cppjieba_rb', require: false gem 'lograge', require: false +gem 'logstash-event', require: false gem 'logstash-logger', require: false gem 'logster' diff --git a/Gemfile.lock b/Gemfile.lock index 506f502ea2..7f9579f54b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -166,7 +166,7 @@ GEM mail (2.6.6) mime-types (>= 1.16, < 4) memory_profiler (0.9.8) - message_bus (2.0.8) + message_bus (2.0.9) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) @@ -177,6 +177,8 @@ GEM mini_portile2 (2.3.0) mini_racer (0.1.11) libv8 (~> 5.7) + mini_suffix (0.3.0) + ffi (~> 1.9) minitest (5.10.3) mocha (1.2.1) metaclass (~> 0.0.1) @@ -230,7 +232,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.19) + onebox (1.8.28) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -259,7 +261,7 @@ GEM puma (3.9.1) r2 (0.2.6) rack (2.0.3) - rack-mini-profiler (0.10.5) + rack-mini-profiler (0.10.7) rack (>= 1.2.0) rack-openid (1.3.1) rack (>= 1.1.0) @@ -273,7 +275,7 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rails_multisite (1.1.0.rc4) + rails_multisite (1.1.2) activerecord (> 4.2, < 6) railties (> 4.2, < 6) railties (5.1.4) @@ -284,7 +286,7 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - raindrops (0.18.0) + raindrops (0.19.0) rake (12.1.0) rake-compiler (1.0.4) rake @@ -352,7 +354,7 @@ GEM bundler ffi (~> 1.9.6) sass (>= 3.3.0) - seed-fu (2.3.6) + seed-fu (2.3.7) activerecord (>= 3.1) activesupport (>= 3.1) shoulda (3.5.0) @@ -388,7 +390,7 @@ GEM unf_ext unf_ext (0.0.7.4) unicode-display_width (1.3.0) - unicorn (5.3.0) + unicorn (5.3.1) kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.10.0) @@ -441,6 +443,7 @@ DEPENDENCIES http_accept_language (~> 2.0.5) listen lograge + logstash-event logstash-logger logster lru_redux @@ -450,6 +453,7 @@ DEPENDENCIES mime-types mini_mime mini_racer + mini_suffix minitest mocha mock_redis @@ -466,7 +470,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.19) + onebox (= 1.8.28) openid-redis-store pg pry-nav @@ -475,7 +479,7 @@ DEPENDENCIES r2 (~> 0.2.5) rack-mini-profiler rack-protection - rails_multisite (~> 1.1.0.rc4) + rails_multisite railties (~> 5.1) rake rb-fsevent @@ -493,7 +497,7 @@ DEPENDENCIES ruby-readability sanitize sassc - seed-fu (~> 2.3.5) + seed-fu shoulda sidekiq simple-rss @@ -507,4 +511,4 @@ DEPENDENCIES webmock BUNDLED WITH - 1.15.4 + 1.16.0 diff --git a/app/assets/javascripts/admin/components/admin-group-selector.js.es6 b/app/assets/javascripts/admin/components/admin-group-selector.js.es6 deleted file mode 100644 index fd487640d0..0000000000 --- a/app/assets/javascripts/admin/components/admin-group-selector.js.es6 +++ /dev/null @@ -1,43 +0,0 @@ -export default Ember.Component.extend({ - tagName: 'div', - - _init: function(){ - this.$("input").select2({ - multiple: true, - width: '100%', - query: function(opts) { - opts.callback({ - results: this.get("available").filter(function(o) { - return -1 !== o.name.toLowerCase().indexOf(opts.term.toLowerCase()); - }).map(this._format) - }); - }.bind(this) - }).on("change", function(evt) { - if (evt.added){ - this.triggerAction({ - action: "groupAdded", - actionContext: this.get("available").findBy("id", evt.added.id) - }); - } else if (evt.removed) { - this.triggerAction({ - action:"groupRemoved", - actionContext: evt.removed.id - }); - } - }.bind(this)); - - this._refreshOnReset(); - }.on("didInsertElement"), - - _format(item) { - return { - "text": item.name, - "id": item.id, - "locked": item.automatic - }; - }, - - _refreshOnReset: function() { - this.$("input").select2("data", this.get("selected").map(this._format)); - }.observes("selected") -}); diff --git a/app/assets/javascripts/admin/components/admin-nav.js.es6 b/app/assets/javascripts/admin/components/admin-nav.js.es6 new file mode 100644 index 0000000000..9250c1ae73 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-nav.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: '' +}); diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6 index 5bf1f9ab22..c8583199b0 100644 --- a/app/assets/javascripts/admin/components/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post.js.es6 @@ -32,12 +32,8 @@ export default Ember.Component.extend({ }, actions: { - showAgreeFlagModal() { - this._spawnModal('admin-agree-flag', this.get('flaggedPost'), 'agree-flag-modal'); - }, - - showDeleteFlagModal() { - this._spawnModal('admin-delete-flag', this.get('flaggedPost'), 'delete-flag-modal'); + removeAfter(promise) { + this.removeAfter(promise); }, disagree() { diff --git a/app/assets/javascripts/admin/components/list-setting.js.es6 b/app/assets/javascripts/admin/components/list-setting.js.es6 deleted file mode 100644 index 9a1d865133..0000000000 --- a/app/assets/javascripts/admin/components/list-setting.js.es6 +++ /dev/null @@ -1,52 +0,0 @@ -/** - Provide a nice GUI for a pipe-delimited list in the site settings. - - @param settingValue is a reference to SiteSetting.value. - @param choices is a reference to SiteSetting.choices -**/ -export default Ember.Component.extend({ - - _select2FormatSelection: function(selectedObject, jqueryWrapper, htmlEscaper) { - var text = selectedObject.text; - if (text.length <= 6) { - jqueryWrapper.closest('li.select2-search-choice').css({"border-bottom": '7px solid #'+text}); - } - return htmlEscaper(text); - }, - - _initializeSelect2: function(){ - var options = { - multiple: false, - separator: "|", - tokenSeparators: ["|"], - tags : this.get("choices") || [], - width: 'off', - dropdownCss: this.get("choices") ? {} : {display: 'none'}, - selectOnBlur: this.get("choices") ? false : true - }; - - var settingName = this.get('settingName'); - if (typeof settingName === 'string' && settingName.indexOf('colors') > -1) { - options.formatSelection = this._select2FormatSelection; - } - - var self = this; - this.$("input").select2(options).on("change", function(obj) { - self.set("settingValue", obj.val.join("|")); - self.refreshSortables(); - }); - - this.refreshSortables(); - }.on('didInsertElement'), - - refreshOnReset: function() { - this.$("input").select2("val", this.get("settingValue").split("|")); - }.observes("settingValue"), - - refreshSortables: function() { - var self = this; - this.$("ul.select2-choices").sortable().on('sortupdate', function() { - self.$("input").select2("onSortEnd"); - }); - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 index 2e39c15880..d06b8dc06e 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 @@ -7,7 +7,7 @@ import computed from 'ember-addons/ember-computed-decorators'; const PROBLEMS_CHECK_MINUTES = 1; -const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'blocked', 'suspended', 'top_traffic_sources', +const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'silenced', 'suspended', 'top_traffic_sources', 'top_referred_topics', 'updated_at']; const REPORTS = [ 'global_reports', 'page_view_reports', 'private_message_reports', 'http_reports', diff --git a/app/assets/javascripts/admin/controllers/admin-logs-search-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-search-logs.js.es6 new file mode 100644 index 0000000000..1de32762a6 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-logs-search-logs.js.es6 @@ -0,0 +1,4 @@ +export default Ember.Controller.extend({ + loading: false, + period: "all" +}); diff --git a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 index 4899c4b59e..626fe955ce 100644 --- a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 @@ -30,7 +30,7 @@ export default Ember.Controller.extend({ showInstructions: Ember.computed.gt('model.length', 0), - refresh: function() { + _refresh() { this.set('loading', true); var filters = this.get('filters'), @@ -65,14 +65,18 @@ export default Ember.Controller.extend({ }); }, + scheduleRefresh() { + Ember.run.scheduleOnce('afterRender', this, this._refresh); + }, + resetFilters: function() { this.set('filters', Ember.Object.create()); - this.refresh(); + this.scheduleRefresh(); }.on('init'), _changeFilters: function(props) { this.get('filters').setProperties(props); - this.refresh(); + this.scheduleRefresh(); }, actions: { @@ -91,7 +95,7 @@ export default Ember.Controller.extend({ this._changeFilters(changed); }, - clearAllFilters: function() { + clearAllFilters() { this.set("filterActionId", null); this.resetFilters(); }, diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index 69c2bca9b8..266fdbd02c 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -58,8 +58,8 @@ export default Ember.Controller.extend(CanCheckEmails, { saveTrustLevel() { return this.get("model").saveTrustLevel(); }, restoreTrustLevel() { return this.get("model").restoreTrustLevel(); }, lockTrustLevel(locked) { return this.get("model").lockTrustLevel(locked); }, - unblock() { return this.get("model").unblock(); }, - block() { return this.get("model").block(); }, + unsilence() { return this.get("model").unsilence(); }, + silence() { return this.get("model").silence(); }, deleteAllPosts() { return this.get("model").deleteAllPosts(); }, anonymize() { return this.get('model').anonymize(); }, destroy() { return this.get('model').destroy(); }, @@ -70,7 +70,9 @@ export default Ember.Controller.extend(CanCheckEmails, { unsuspend() { this.get("model").unsuspend().catch(popupAjaxError); }, - + showSilenceModal() { + this.get('adminTools').showSilenceModal(this.get('model')); + }, toggleUsernameEdit() { this.set('userUsernameValue', this.get('model.username')); diff --git a/app/assets/javascripts/admin/controllers/admin.js.es6 b/app/assets/javascripts/admin/controllers/admin.js.es6 index 3bdfbe6b50..0e5d3c6b50 100644 --- a/app/assets/javascripts/admin/controllers/admin.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin.js.es6 @@ -1,5 +1,20 @@ +import computed from 'ember-addons/ember-computed-decorators'; + export default Ember.Controller.extend({ - showBadges: function() { - return this.get('currentUser.admin') && this.siteSettings.enable_badges; - }.property() + application: Ember.inject.controller(), + + @computed + showBadges() { + return this.currentUser.get('admin') && this.siteSettings.enable_badges; + }, + + @computed('application.currentPath') + adminContentsClassName(currentPath) { + return currentPath.split('.').filter(segment => { + return segment !== 'index' && + segment !== 'loading' && + segment !== 'show' && + segment !== 'admin'; + }).map(Ember.String.dasherize).join(' '); + } }); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 deleted file mode 100644 index 07fbe0763c..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DeleteSpammerModal from 'admin/mixins/delete-spammer-modal'; - -export default Ember.Controller.extend(ModalFunctionality, DeleteSpammerModal, { - removeAfter: null, - - actions: { - agreeDeleteSpammer(user) { - return this.removeAfter(user.deleteAsSpammer()).then(() => { - this.send('closeModal'); - }); - }, - - perform(action) { - let flaggedPost = this.get('model'); - return this.removeAfter(flaggedPost.agreeFlags(action)).then(() => { - this.send('closeModal'); - }); - }, - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 deleted file mode 100644 index e8038edbca..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 +++ /dev/null @@ -1,22 +0,0 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DeleteSpammerModal from 'admin/mixins/delete-spammer-modal'; - -export default Ember.Controller.extend(ModalFunctionality, DeleteSpammerModal, { - removeAfter: null, - - actions: { - deletePostDeferFlag() { - let flaggedPost = this.get('model'); - this.removeAfter(flaggedPost.deferFlags(true)).then(() => { - this.send('closeModal'); - }); - }, - - deletePostAgreeFlag() { - let flaggedPost = this.get('model'); - this.removeAfter(flaggedPost.agreeFlags('delete')).then(() => { - this.send('closeModal'); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 new file mode 100644 index 0000000000..9f1aef916f --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 @@ -0,0 +1,50 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import computed from 'ember-addons/ember-computed-decorators'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Controller.extend(ModalFunctionality, { + silenceUntil: null, + reason: null, + message: null, + silencing: false, + user: null, + post: null, + successCallback: null, + + onShow() { + this.setProperties({ + silenceUntil: null, + reason: null, + message: null, + silencing: false, + loadingUser: true, + post: null, + successCallback: null, + }); + }, + + @computed('silenceUntil', 'reason', 'silencing') + submitDisabled(silenceUntil, reason, silencing) { + return (silencing || Ember.isEmpty(silenceUntil) || !reason || reason.length < 1); + }, + + actions: { + silence() { + if (this.get('submitDisabled')) { return; } + + this.set('silencing', true); + this.get('user').silence({ + silenced_till: this.get('silenceUntil'), + reason: this.get('reason'), + message: this.get('message'), + post_id: this.get('post.id') + }).then(result => { + this.send('closeModal'); + let callback = this.get('successCallback'); + if (callback) { + callback(result); + } + }).catch(popupAjaxError).finally(() => this.set('silencing', false)); + } + } +}); diff --git a/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 b/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 deleted file mode 100644 index 138fd5422a..0000000000 --- a/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -export default Ember.Mixin.create({ - adminTools: Ember.inject.service(), - spammerDetails: null, - - onShow() { - let adminTools = this.get('adminTools'); - let spammerDetails = adminTools.spammerDetails(this.get('model.user')); - - this.setProperties({ - spammerDetails, - canDeleteSpammer: spammerDetails.canDelete && this.get('model.flaggedForSpam') - }); - }, - - actions: { - deleteSpammer() { - let spammerDetails = this.get('spammerDetails'); - this.removeAfter(spammerDetails.deleteUser()).then(() => { - this.send('closeModal'); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 893631b785..e3f1530bad 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -8,6 +8,8 @@ import Group from 'discourse/models/group'; import TL3Requirements from 'admin/models/tl3-requirements'; import { userPath } from 'discourse/lib/url'; +const wrapAdmin = user => user ? AdminUser.create(user) : null; + const AdminUser = Discourse.User.extend({ adminUserView: true, customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)), @@ -232,6 +234,7 @@ const AdminUser = Discourse.User.extend({ }.property('trust_level'), isSuspended: Em.computed.equal('suspended', true), + isSilenced: Ember.computed.equal('silenced', true), canSuspend: Em.computed.not('staff'), suspendDuration: function() { @@ -299,46 +302,38 @@ const AdminUser = Discourse.User.extend({ }); }, - unblock() { - this.set('blockingUser', true); - return ajax('/admin/users/' + this.id + '/unblock', { + unsilence() { + this.set('silencingUser', true); + + return ajax(`/admin/users/${this.id}/unsilence`, { type: 'PUT' - }).then(function() { - window.location.reload(); - }).catch(function(e) { - var error = I18n.t('admin.user.unblock_failed', { error: "http: " + e.status + " - " + e.body }); + }).then(result => { + this.setProperties(result.unsilence); + }).catch(e => { + let error = I18n.t('admin.user.unsilence_failed', { + error: `http: ${e.status} - ${e.body}` + }); bootbox.alert(error); + }).finally(() => { + this.set('silencingUser', false); }); }, - block() { - const user = this, - message = I18n.t("admin.user.block_confirm"); - - const performBlock = function() { - user.set('blockingUser', true); - return ajax('/admin/users/' + user.id + '/block', { - type: 'PUT' - }).then(function() { - window.location.reload(); - }).catch(function(e) { - var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body }); - bootbox.alert(error); - user.set('blockingUser', false); + silence(data) { + this.set('silencingUser', true); + return ajax(`/admin/users/${this.id}/silence`, { + type: 'PUT', + data + }).then(result => { + this.setProperties(result.silence); + }).catch(e => { + let error = I18n.t('admin.user.silence_failed', { + error: `http: ${e.status} - ${e.body}` }); - }; - - const buttons = [{ - "label": I18n.t("composer.cancel"), - "class": "cancel", - "link": true - }, { - "label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.block_accept'), - "class": "btn btn-danger", - "callback": function() { performBlock(); } - }]; - - bootbox.dialog(message, buttons, { "classes": "delete-user-modal" }); + bootbox.alert(error); + }).finally(() => { + this.set('silencingUser', false); + }); }, sendActivationEmail() { @@ -475,17 +470,14 @@ const AdminUser = Discourse.User.extend({ } }.property('tl3_requirements'), - suspendedBy: function() { - if (this.get('suspended_by')) { - return AdminUser.create(this.get('suspended_by')); - } - }.property('suspended_by'), + @computed('suspended_by') + suspendedBy: wrapAdmin, - approvedBy: function() { - if (this.get('approved_by')) { - return AdminUser.create(this.get('approved_by')); - } - }.property('approved_by') + @computed('silenced_by') + silencedBy: wrapAdmin, + + @computed('approved_by') + approvedBy: wrapAdmin, }); diff --git a/app/assets/javascripts/admin/models/version-check.js.es6 b/app/assets/javascripts/admin/models/version-check.js.es6 index 75f0d9e04f..ce1f420b49 100644 --- a/app/assets/javascripts/admin/models/version-check.js.es6 +++ b/app/assets/javascripts/admin/models/version-check.js.es6 @@ -8,18 +8,6 @@ const VersionCheck = Discourse.Model.extend({ return updatedAt === null; }, - @computed('updated_at', 'version_check_pending') - dataIsOld(updatedAt, versionCheckPending) { - return versionCheckPending || moment().diff(moment(updatedAt), 'hours') >= 48; - }, - - @computed('dataIsOld', 'installed_version', 'latest_version', 'missing_versions_count') - staleData(dataIsOld, installedVersion, latestVersion, missingVersionsCount) { - return dataIsOld || - (installedVersion !== latestVersion && missingVersionsCount === 0) || - (installedVersion === latestVersion && missingVersionsCount !== 0); - }, - @computed('missing_versions_count') upToDate(missingVersionsCount) { return missingVersionsCount === 0 || missingVersionsCount === null; diff --git a/app/assets/javascripts/admin/routes/admin-logs-search-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-search-logs.js.es6 new file mode 100644 index 0000000000..013bdb4655 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-logs-search-logs.js.es6 @@ -0,0 +1,25 @@ +import { ajax } from 'discourse/lib/ajax'; + +export default Discourse.Route.extend({ + renderTemplate() { + this.render('admin/templates/logs/search-logs', {into: 'adminLogs'}); + }, + + queryParams: { + period: { + refreshModel: true + } + }, + + model(params) { + this._params = params; + return ajax('/admin/logs/search_logs.json', { data: { period: params.period } }).then(search_logs => { + return search_logs.map(sl => Ember.Object.create(sl)); + }); + }, + + setupController(controller, model) { + const params = this._params; + controller.setProperties({ model, period: params.period }); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index 7628ca8389..b221ba639c 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -66,6 +66,7 @@ export default function() { this.route('screenedEmails', { path: '/screened_emails' }); this.route('screenedIpAddresses', { path: '/screened_ip_addresses' }); this.route('screenedUrls', { path: '/screened_urls' }); + this.route('searchLogs', { path: '/search_logs' }); this.route('adminWatchedWords', { path: '/watched_words', resetNamespace: true}, function() { this.route('index', { path: '/' } ); this.route('action', { path: '/action/:action_id' } ); diff --git a/app/assets/javascripts/admin/routes/admin-users-index.js.es6 b/app/assets/javascripts/admin/routes/admin-users-index.js.es6 new file mode 100644 index 0000000000..8bb7adc055 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-users-index.js.es6 @@ -0,0 +1,5 @@ +export default Discourse.Route.extend({ + redirect: function() { + this.transitionTo('adminUsersList'); + } +}); diff --git a/app/assets/javascripts/admin/services/admin-tools.js.es6 b/app/assets/javascripts/admin/services/admin-tools.js.es6 index 9f855772a2..bc524706f9 100644 --- a/app/assets/javascripts/admin/services/admin-tools.js.es6 +++ b/app/assets/javascripts/admin/services/admin-tools.js.es6 @@ -6,9 +6,17 @@ import AdminUser from 'admin/models/admin-user'; import { iconHTML } from 'discourse-common/lib/icon-library'; import { ajax } from 'discourse/lib/ajax'; import showModal from 'discourse/lib/show-modal'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Service.extend({ + init() { + this._super(); + + // TODO: Make `siteSettings` a service that can be injected + this.siteSettings = getOwner(this).lookup('site-settings:main'); + }, + checkSpammer(userId) { return AdminUser.find(userId).then(au => this.spammerDetails(au)); }, @@ -20,12 +28,12 @@ export default Ember.Service.extend({ }; }, - showSuspendModal(user, opts) { + _showControlModal(type, user, opts) { opts = opts || {}; - let controller = showModal('admin-suspend-user', { + let controller = showModal(`admin-${type}-user`, { admin: true, - modalClass: 'suspend-user-modal' + modalClass: `${type}-user-modal` }); if (opts.post) { controller.set('post', opts.post); @@ -44,8 +52,22 @@ export default Ember.Service.extend({ }); }, + showSilenceModal(user, opts) { + this._showControlModal('silence', user, opts); + }, + + showSuspendModal(user, opts) { + this._showControlModal('suspend', user, opts); + }, + _deleteSpammer(adminUser) { - return adminUser.checkEmail().then(() => { + + // Try loading the email if the site supports it + let tryEmail = this.siteSettings.show_email_on_profile ? + adminUser.checkEmail() : + Ember.RSVP.resolve(); + + return tryEmail.then(() => { let message = I18n.messageFormat('flagging.delete_confirm_MF', { "POSTS": adminUser.get('post_count'), diff --git a/app/assets/javascripts/admin/templates/admin.hbs b/app/assets/javascripts/admin/templates/admin.hbs index 780aad41fc..01f9ffb673 100644 --- a/app/assets/javascripts/admin/templates/admin.hbs +++ b/app/assets/javascripts/admin/templates/admin.hbs @@ -27,7 +27,7 @@
-
+
{{outlet}}
diff --git a/app/assets/javascripts/admin/templates/api.hbs b/app/assets/javascripts/admin/templates/api.hbs index f3407fe0cc..45bfbd9ca7 100644 --- a/app/assets/javascripts/admin/templates/api.hbs +++ b/app/assets/javascripts/admin/templates/api.hbs @@ -1,10 +1,8 @@ -
- {{#admin-nav}} - {{nav-item route='adminApiKeys' label='admin.api.title'}} - {{nav-item route='adminWebHooks' label='admin.web_hooks.title'}} - {{/admin-nav}} +{{#admin-nav}} + {{nav-item route='adminApiKeys' label='admin.api.title'}} + {{nav-item route='adminWebHooks' label='admin.web_hooks.title'}} +{{/admin-nav}} -
- {{outlet}} -
+
+ {{outlet}}
diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs index 4a832771a8..29e7213189 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs @@ -20,6 +20,10 @@
+
+ {{format-username flaggedPost.user.username}} +
+
{{#unless hideTitle}}

@@ -107,13 +111,9 @@ {{#if canAct}}
- {{d-button - title="admin.flags.agree_title" - class="agree-flag" - label="admin.flags.agree" - icon="thumbs-o-up" - action="showAgreeFlagModal" - ellipsis=true}} + {{admin-agree-flag-dropdown + post=flaggedPost + removeAfter=(action "removeAfter") }} {{#if flaggedPost.postHidden}} {{d-button @@ -138,12 +138,7 @@ icon="external-link" label="admin.flags.defer_flag"}} - {{d-button - class="btn-danger delete-flag" - title="admin.flags.delete_title" - action="showDeleteFlagModal" - icon="trash-o" - label="admin.flags.delete"}} + {{admin-delete-flag-dropdown post=flaggedPost removeAfter=(action "removeAfter")}} {{#unless suspended}} {{d-button diff --git a/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs b/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs index ac6935672c..ad9f40585a 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-topic-users.hbs @@ -1,5 +1,5 @@ {{#each users as |u|}} - {{#link-to 'adminUser' u class="flagged-topic-user"}} + {{#link-to 'adminUser' u.id u.username class="flagged-topic-user"}} {{avatar u imageSize="small"}} {{/link-to}} {{/each}} diff --git a/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs index 8c0b4eda1e..0aadec18d8 100644 --- a/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs @@ -1,3 +1,3 @@ -{{category-selector categories=selectedCategories blacklist=selectedCategories}} +{{category-selector categories=selectedCategories}}
{{{unbound setting.description}}}
{{setting-validation-message message=validationMessage}} diff --git a/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs b/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs index 765a0e20d1..9b3da3178e 100644 --- a/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs @@ -1,4 +1,4 @@ -{{combo-box valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}} +{{combo-box castInteger=true valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}} {{preview}} {{setting-validation-message message=validationMessage}}
{{{unbound setting.description}}}
diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs index 3065c09855..d5d3816310 100644 --- a/app/assets/javascripts/admin/templates/customize.hbs +++ b/app/assets/javascripts/admin/templates/customize.hbs @@ -1,16 +1,14 @@ -
- {{#admin-nav}} - {{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}} - {{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}} - {{nav-item route='adminSiteText' label='admin.site_text.title'}} - {{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}} - {{nav-item route='adminUserFields' label='admin.user_fields.title'}} - {{nav-item route='adminEmojis' label='admin.emoji.title'}} - {{nav-item route='adminPermalinks' label='admin.permalink.title'}} - {{nav-item route='adminEmbedding' label='admin.embedding.title'}} - {{/admin-nav}} +{{#admin-nav}} + {{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}} + {{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}} + {{nav-item route='adminSiteText' label='admin.site_text.title'}} + {{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}} + {{nav-item route='adminUserFields' label='admin.user_fields.title'}} + {{nav-item route='adminEmojis' label='admin.emoji.title'}} + {{nav-item route='adminPermalinks' label='admin.permalink.title'}} + {{nav-item route='adminEmbedding' label='admin.embedding.title'}} +{{/admin-nav}} -
- {{outlet}} -
+
+ {{outlet}}
diff --git a/app/assets/javascripts/admin/templates/dashboard.hbs b/app/assets/javascripts/admin/templates/dashboard.hbs index b8c0297d48..6b4dc88584 100644 --- a/app/assets/javascripts/admin/templates/dashboard.hbs +++ b/app/assets/javascripts/admin/templates/dashboard.hbs @@ -37,8 +37,8 @@ {{d-icon "shield"}} {{i18n 'admin.dashboard.moderators'}} {{#link-to 'adminUsersList.show' 'moderators'}}{{moderators}}{{/link-to}} - {{d-icon "ban"}} {{i18n 'admin.dashboard.blocked'}} - {{#link-to 'adminUsersList.show' 'blocked'}}{{blocked}}{{/link-to}} + {{d-icon "ban"}} {{i18n 'admin.dashboard.silenced'}} + {{#link-to 'adminUsersList.show' 'silenced'}}{{silenced}}{{/link-to}}
diff --git a/app/assets/javascripts/admin/templates/flags-topics-index.hbs b/app/assets/javascripts/admin/templates/flags-topics-index.hbs index d9fdf87066..b5a7eca133 100644 --- a/app/assets/javascripts/admin/templates/flags-topics-index.hbs +++ b/app/assets/javascripts/admin/templates/flags-topics-index.hbs @@ -37,7 +37,8 @@ ft.id class="btn d-button no-text btn-small btn-primary show-details" title=(i18n "admin.flags.show_details")}} - {{d-icon "search"}} + {{d-icon "list"}} + {{i18n "admin.flags.details"}} {{/link-to}} diff --git a/app/assets/javascripts/admin/templates/logs.hbs b/app/assets/javascripts/admin/templates/logs.hbs index d72e5a9262..362c376769 100644 --- a/app/assets/javascripts/admin/templates/logs.hbs +++ b/app/assets/javascripts/admin/templates/logs.hbs @@ -4,6 +4,7 @@ {{nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}} {{nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}} {{nav-item route='adminWatchedWords' label='admin.watched_words.title'}} + {{nav-item route='adminLogs.searchLogs' label='admin.logs.search_logs.title'}} {{#if currentUser.admin}} {{nav-item path='/logs' label='admin.logs.logster.title'}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/logs/search-logs.hbs b/app/assets/javascripts/admin/templates/logs/search-logs.hbs new file mode 100644 index 0000000000..5b606de9d6 --- /dev/null +++ b/app/assets/javascripts/admin/templates/logs/search-logs.hbs @@ -0,0 +1,36 @@ +

+ {{period-chooser period=period}} +

+
+ +{{#conditional-loading-spinner condition=loading}} + {{#if model.length}} + +
+
+
{{i18n 'admin.logs.search_logs.term'}}
+
{{i18n 'admin.logs.search_logs.searches'}}
+
{{i18n 'admin.logs.search_logs.click_through'}}
+
{{i18n 'admin.logs.search_logs.most_viewed_topic'}}
+
{{i18n 'admin.logs.search_logs.unique'}}
+
+ + {{#each model as |item|}} +
+
{{item.term}}
+
{{item.searches}}
+
{{item.click_through}}
+
+ {{#if item.clicked_topic_id}} + {{item.topic_title}} + {{/if}} +
+
{{item.unique}}
+
+ {{/each}} +
+ + {{else}} + {{i18n 'search.no_results'}} + {{/if}} +{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs index c44edc1a3e..5df150c79f 100644 --- a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs @@ -1,6 +1,6 @@
{{#if filtersExists}} -
+
{{i18n 'admin.logs.staff_actions.clear_filters'}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs deleted file mode 100644 index b9d5a88383..0000000000 --- a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs +++ /dev/null @@ -1,35 +0,0 @@ -{{#d-modal-body title="admin.flags.agree_flag_modal_title"}} - {{#if model.user_deleted}} - {{d-button - title="admin.flags.agree_flag_restore_post_title" - class="confirm-agree-restore" - action=(action "perform" "restore") - icon="eye" - label="admin.flags.agree_flag_restore_post"}} - {{else}} - {{#unless model.postHidden}} - {{d-button - title="admin.flags.agree_flag_hide_post_title" - action=(action "perform" "hide") - class="confirm-agree-hide" - icon="eye-slash" - label="admin.flags.agree_flag_hide_post"}} - {{/unless}} - {{/if}} - - {{d-button - title="admin.flags.agree_flag_title" - action=(action "perform" "keep") - class="confirm-agree-keep" - icon="thumbs-o-up" - label="admin.flags.agree_flag"}} - - {{#if canDeleteSpammer}} - {{d-button - title="admin.flags.delete_spammer_title" - action="deleteSpammer" - class="btn-danger delete-spammer" - icon="exclamation-triangle" - label="admin.flags.delete_spammer"}} - {{/if}} -{{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs deleted file mode 100644 index 571f332dbf..0000000000 --- a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs +++ /dev/null @@ -1,24 +0,0 @@ -{{#d-modal-body title="admin.flags.delete_flag_modal_title"}} - {{d-button - class="delete-defer" - title="admin.flags.delete_post_defer_flag_title" - action="deletePostDeferFlag" - icon="external-link" - label="admin.flags.delete_post_defer_flag"}} - - {{d-button - class="delete-agree" - title="admin.flags.delete_post_agree_flag_title" - action="deletePostAgreeFlag" - icon="thumbs-o-up" - label="admin.flags.delete_post_agree_flag"}} - - {{#if canDeleteSpammer}} - {{d-button - class="btn-danger delete-spammer" - title="admin.flags.delete_spammer_title" - action="deleteSpammer" - icon="exclamation-triangle" - label="admin.flags.delete_spammer"}} - {{/if}} -{{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs new file mode 100644 index 0000000000..0abe77544c --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs @@ -0,0 +1,50 @@ +{{#d-modal-body title="admin.user.silence_modal_title"}} + {{#conditional-loading-spinner condition=loadingUser}} + +
+ +
+ +
+ + {{text-field + value=reason + class="silence-reason" + placeholderKey="admin.user.silence_reason_placeholder"}} +
+ + + {{textarea + value=message + class="silence-message" + placeholder=(i18n "admin.user.silence_message_placeholder")}} + + + {{/conditional-loading-spinner}} + +{{/d-modal-body}} + + \ No newline at end of file diff --git a/app/assets/javascripts/admin/templates/modal/admin-staff-action-log-details.hbs b/app/assets/javascripts/admin/templates/modal/admin-staff-action-log-details.hbs index f63be2d098..e8e8cd8d2e 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-staff-action-log-details.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-staff-action-log-details.hbs @@ -2,5 +2,5 @@
{{model.details}}
{{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs index a701cf5045..fc6025a6a6 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs @@ -21,23 +21,22 @@ {{{i18n 'admin.user.suspend_reason_label'}}} {{/if}}
- - {{text-field - value=reason - class="suspend-reason" - placeholderKey="admin.user.suspend_reason_placeholder"}} + {{text-field + value=reason + class="suspend-reason" + placeholderKey="admin.user.suspend_reason_placeholder"}}
+ {{textarea value=message class="suspend-message" placeholder=(i18n "admin.user.suspend_message_placeholder")}} - {{else}}
{{i18n "admin.user.cant_suspend"}} diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index ec086e3271..9a1325c03f 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -17,6 +17,7 @@ {{d-button action="logOut" icon="power-off" label="admin.user.log_out"}} {{/if}} {{/if}} + {{plugin-outlet name="admin-user-controls-after" args=(hash model=model) tagName="" connectorTagName=""}}
@@ -287,7 +288,7 @@
{{#if model.canLockTrustLevel}} - {{#if model.trust_level_locked}} + {{#if model.manual_locked_trust_level}} {{d-icon "lock" title="admin.user.trust_level_locked_tip"}} {{d-button action="lockTrustLevel" actionParam=false label="admin.user.unlock_trust_level"}} {{else}} @@ -346,21 +347,51 @@
{{/if}} -
-
{{i18n 'admin.user.blocked'}}
-
{{i18n-yes-no model.blocked}}
+
+
{{i18n 'admin.user.silenced'}}
+
+ {{i18n-yes-no model.silenced}} + {{#if model.isSilenced}} + {{#unless model.silencedForever}} + {{i18n "admin.user.suspended_until" until=model.silencedTillDate}} + {{/unless}} + {{/if}} +
- {{#conditional-loading-spinner size="small" condition=model.blockingUser}} - {{#if model.blocked}} - {{d-button action="unblock" icon="thumbs-o-up" label="admin.user.unblock"}} - {{i18n 'admin.user.block_explanation'}} + {{#conditional-loading-spinner size="small" condition=model.silencingUser}} + {{#if model.silenced}} + {{d-button + class="btn-danger unsilence-user" + action="unsilence" + icon="microphone-slash" + label="admin.user.unsilence"}} + {{i18n 'admin.user.silence_explanation'}} {{else}} - {{d-button action="block" icon="ban" label="admin.user.block"}} - {{i18n 'admin.user.block_explanation'}} + {{d-button + class="btn-danger silence-user" + action=(action "showSilenceModal") + icon="microphone-slash" + label="admin.user.silence"}} + {{i18n 'admin.user.silence_explanation'}} {{/if}} {{/conditional-loading-spinner}}
+ + {{#if model.isSilenced}} +
+
{{i18n 'admin.user.silenced_by'}}
+
+ {{#link-to 'adminUser' silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}} + {{#link-to 'adminUser' silencedBy}}{{model.silencedBy.username}}{{/link-to}} +
+
+ {{i18n 'admin.user.silence_reason'}}: + {{model.silence_reason}} +
+
+ {{/if}} + {{#if currentUser.admin}} @@ -398,7 +429,7 @@
{{i18n 'admin.users.last_emailed'}}
-
{{format-date model.last_emailed_at leaveAgo="true"}}
+
{{format-date model.last_emailed_at}}
{{i18n 'last_seen'}}
@@ -443,7 +474,7 @@
{{i18n 'admin.user.time_read'}}
-
{{{model.time_read}}}
+
{{{format-duration model.time_read}}}
{{i18n 'user.invited.days_visited'}}
diff --git a/app/assets/javascripts/admin/templates/users-list-show.hbs b/app/assets/javascripts/admin/templates/users-list-show.hbs index cd292cb3e0..0ccaee0251 100644 --- a/app/assets/javascripts/admin/templates/users-list-show.hbs +++ b/app/assets/javascripts/admin/templates/users-list-show.hbs @@ -53,17 +53,20 @@ {{avatar user imageSize="small"}} - + {{#link-to 'adminUser' user}}{{unbound user.username}}{{/link-to}} + {{#if user.staged}} + {{d-icon "envelope-o" title="user.staged" }} + {{/if}} {{unbound user.email}} - {{{unbound user.last_emailed_age}}} + {{{format-duration user.last_emailed_age}}} - {{{unbound user.last_seen_age}}} + {{{format-duration user.last_seen_age}}} {{number user.topics_entered}} @@ -72,11 +75,11 @@ {{number user.posts_read_count}} - {{{unbound user.time_read}}} + {{{format-duration user.time_read}}} - {{{unbound user.created_at_age}}} + {{{format-duration user.created_at_age}}} {{#if showApproval}} diff --git a/app/assets/javascripts/admin/templates/users-list.hbs b/app/assets/javascripts/admin/templates/users-list.hbs index d37a9ae267..398732685b 100644 --- a/app/assets/javascripts/admin/templates/users-list.hbs +++ b/app/assets/javascripts/admin/templates/users-list.hbs @@ -8,7 +8,7 @@ {{/if}} {{nav-item route='adminUsersList.show' routeParam='staff' label='admin.users.nav.staff'}} {{nav-item route='adminUsersList.show' routeParam='suspended' label='admin.users.nav.suspended'}} - {{nav-item route='adminUsersList.show' routeParam='blocked' label='admin.users.nav.blocked'}} + {{nav-item route='adminUsersList.show' routeParam='silenced' label='admin.users.nav.silenced'}} {{nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}}
diff --git a/app/assets/javascripts/admin/templates/version-checks.hbs b/app/assets/javascripts/admin/templates/version-checks.hbs index 4f39265026..a8724b7e5a 100644 --- a/app/assets/javascripts/admin/templates/version-checks.hbs +++ b/app/assets/javascripts/admin/templates/version-checks.hbs @@ -24,7 +24,7 @@ {{i18n 'admin.dashboard.no_check_performed'}} {{else}} - {{#if versionCheck.staleData}} + {{#if versionCheck.stale_data}} {{#if versionCheck.version_check_pending}}{{dash-if-empty versionCheck.installed_version}}{{/if}} {{#if versionCheck.version_check_pending}} diff --git a/app/assets/javascripts/admin/templates/web-hooks-show.hbs b/app/assets/javascripts/admin/templates/web-hooks-show.hbs index 53c0cba146..ef9c478774 100644 --- a/app/assets/javascripts/admin/templates/web-hooks-show.hbs +++ b/app/assets/javascripts/admin/templates/web-hooks-show.hbs @@ -48,7 +48,7 @@
- {{category-selector categories=model.categories blacklist=model.categories}} + {{category-selector categories=model.categories}}
{{i18n 'admin.web_hooks.categories_filter_instructions'}}
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 280e12954e..6b0d64be84 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -4,7 +4,7 @@ //= require ./ember-addons/ember-computed-decorators //= require ./ember-addons/fmt //= require_tree ./discourse-common -//= require_tree ./select-box-kit +//= require_tree ./select-kit //= require ./discourse //= require ./deprecated diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 0dcf4051cd..84242d03f6 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -42,7 +42,8 @@ export function renderIcon(renderType, id, params) { let rendererForType = renderer[renderType]; if (rendererForType) { - let result = rendererForType(REPLACEMENTS[id] || id, params || {}); + const icon = { id, replacementId: REPLACEMENTS[id] }; + let result = rendererForType(icon, params || {}); if (result) { return result; } @@ -68,8 +69,9 @@ export function registerIconRenderer(renderer) { } // Support for font awesome icons -function faClasses(id, params) { - let classNames = `fa fa-${id} d-icon d-icon-${id}`; +function faClasses(icon, params) { + let classNames = `fa fa-${icon.replacementId || icon.id} d-icon d-icon-${icon.id}`; + if (params) { if (params.modifier) { classNames += " fa-" + params.modifier; } if (params['class']) { classNames += ' ' + params['class']; } @@ -81,9 +83,9 @@ function faClasses(id, params) { registerIconRenderer({ name: 'font-awesome', - string(id, params) { + string(icon, params) { let tagName = params.tagName || 'i'; - let html = `<${tagName} class='${faClasses(id, params)}'`; + let html = `<${tagName} class='${faClasses(icon, params)}'`; if (params.title) { html += ` title='${I18n.t(params.title)}'`; } if (params.label) { html += " aria-hidden='true'"; } html += `>`; @@ -93,11 +95,11 @@ registerIconRenderer({ return html; }, - node(id, params) { + node(icon, params) { let tagName = params.tagName || 'i'; const properties = { - className: faClasses(id, params), + className: faClasses(icon, params), attributes: { "aria-hidden": true } }; diff --git a/app/assets/javascripts/discourse/components/badge-title.js.es6 b/app/assets/javascripts/discourse/components/badge-title.js.es6 index 95da5718bb..322db1f6c2 100644 --- a/app/assets/javascripts/discourse/components/badge-title.js.es6 +++ b/app/assets/javascripts/discourse/components/badge-title.js.es6 @@ -11,9 +11,11 @@ export default Ember.Component.extend(BadgeSelectController, { save() { this.setProperties({ saved: false, saving: true }); + var badge_id = this.get('selectedUserBadgeId') || 0; + ajax(this.get('user.path') + "/preferences/badge_title", { type: "PUT", - data: { user_badge_id: this.get('selectedUserBadgeId') } + data: { user_badge_id: badge_id } }).then(() => { this.setProperties({ saved: true, diff --git a/app/assets/javascripts/discourse/components/category-selector.js.es6 b/app/assets/javascripts/discourse/components/category-selector.js.es6 deleted file mode 100644 index 9cb3eec14a..0000000000 --- a/app/assets/javascripts/discourse/components/category-selector.js.es6 +++ /dev/null @@ -1,49 +0,0 @@ -import { categoryBadgeHTML } from 'discourse/helpers/category-link'; -import Category from 'discourse/models/category'; -import { on, observes } from 'ember-addons/ember-computed-decorators'; -import { findRawTemplate } from 'discourse/lib/raw-templates'; - -export default Ember.Component.extend({ - @observes('categories') - _update() { - if (this.get('canReceiveUpdates') === 'true') - this._initializeAutocomplete({updateData: true}); - }, - - @on('didInsertElement') - _initializeAutocomplete(opts) { - const self = this, - regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`); - - this.$('input').autocomplete({ - items: this.get('categories'), - single: this.get('single'), - allowAny: false, - updateData: (opts && opts.updateData) ? opts.updateData : false, - dataSource(term) { - return Category.list().filter(category => { - const regex = new RegExp(term, 'i'); - return category.get('name').match(regex) && - !_.contains(self.get('blacklist') || [], category) && - !_.contains(self.get('categories'), category) ; - }); - }, - onChangeItems(items) { - const categories = _.map(items, link => { - const slug = link.match(regexp)[1]; - return Category.findSingleBySlug(slug); - }); - Em.run.next(() => { - let existingCategory = _.isArray(self.get('categories')) ? self.get('categories') : [self.get('categories')]; - const result = _.intersection(existingCategory.map(itm => itm.id), categories.map(itm => itm.id)); - if (result.length !== categories.length || existingCategory.length !== categories.length) - self.set('categories', categories); - }); - }, - template: findRawTemplate('category-selector-autocomplete'), - transformComplete(category) { - return categoryBadgeHTML(category, {allowUncategorized: true}); - } - }); - } -}); diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6 index 1d193b5257..8df136d372 100644 --- a/app/assets/javascripts/discourse/components/composer-body.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-body.js.es6 @@ -15,6 +15,7 @@ export default Ember.Component.extend(KeyEnterEscape, { 'composer.createdPost:created-post', 'composer.creatingTopic:topic', 'composer.whisper:composing-whisper', + 'showPreview:show-preview:hide-preview', 'currentUserPrimaryGroupClass'], @computed("currentUser.primary_group_name") @@ -41,19 +42,6 @@ export default Ember.Component.extend(KeyEnterEscape, { const h = $('#reply-control').height() || 0; this.movePanels(h + "px"); - - // Figure out the size of the fields - const $fields = this.$('.composer-fields'); - const fieldPos = $fields.position(); - if (fieldPos) { - this.$('.wmd-controls').css('top', $fields.height() + fieldPos.top + 5); - } - - // get the submit panel height - const submitPos = this.$('.submit-panel').position(); - if (submitPos) { - this.$('.wmd-controls').css('bottom', h - submitPos.top + 7); - } }); }, diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 8445b9f8a5..4b95913aa4 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -1,5 +1,5 @@ import userSearch from 'discourse/lib/user-search'; -import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; +import { default as computed, on } from 'ember-addons/ember-computed-decorators'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags'; import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link-tag-hashtag'; @@ -12,58 +12,36 @@ import { findRawTemplate } from 'discourse/lib/raw-templates'; import { tinyAvatar, displayErrorForUpload, getUploadMarkdown, - validateUploadedFiles } from 'discourse/lib/utilities'; -import { lookupCachedUploadUrl, - lookupUncachedUploadUrls, - cacheShortUploadUrl } from 'pretty-text/image-short-url'; + validateUploadedFiles, + formatUsername +} from 'discourse/lib/utilities'; +import { cacheShortUploadUrl, resolveAllShortUrls } from 'pretty-text/image-short-url'; + +const REBUILD_SCROLL_MAP_EVENTS = [ + 'composer:resized', + 'composer:typed-reply' +]; export default Ember.Component.extend({ - classNames: ['wmd-controls'], - classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls', 'showPreview', 'showPreview::hide-preview'], + classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls'], uploadProgress: 0, - showPreview: true, _xhr: null, + shouldBuildScrollMap: true, + scrollMap: null, @computed uploadPlaceholder() { return `[${I18n.t('uploading')}]() `; }, - @on('init') - _setupPreview() { - const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true')); - this.set('showPreview', val === 'true'); - - this.appEvents.on('composer:show-preview', () => { - this.set('showPreview', true); - }); - - this.appEvents.on('composer:hide-preview', () => { - this.set('showPreview', false); - }); - }, - - @computed('site.mobileView', 'showPreview') - forcePreview(mobileView, showPreview) { - return mobileView && showPreview; - }, - - @computed('showPreview') - toggleText: function(showPreview) { - return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview'); - }, - - @observes('showPreview') - showPreviewChanged() { - this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') }); - }, - @computed markdownOptions() { return { previewing: true, + formatUsername, + lookupAvatarByPostNumber: (postNumber, topicId) => { const topic = this.get('topic'); if (!topic) { return; } @@ -75,6 +53,19 @@ export default Ember.Component.extend({ return tinyAvatar(quotedPost.get('avatar_template')); } } + }, + + lookupPrimaryUserGroupByPostNumber: (postNumber, topicId) => { + const topic = this.get('topic'); + if (!topic) { return; } + + const posts = topic.get('postStream.posts'); + if (posts && topicId === topic.get('id')) { + const quotedPost = posts.findBy("post_number", postNumber); + if (quotedPost) { + return quotedPost.primary_group_name; + } + } } }; }, @@ -83,6 +74,8 @@ export default Ember.Component.extend({ _composerEditorInit() { const topicId = this.get('topic.id'); const $input = this.$('.d-editor-input'); + const $preview = this.$('.d-editor-preview'); + $input.autocomplete({ template: findRawTemplate('user-selector-autocomplete'), dataSource: term => userSearch({ @@ -94,7 +87,7 @@ export default Ember.Component.extend({ transformComplete: v => v.username || v.name }); - $input.on('scroll', () => Ember.run.throttle(this, this._syncEditorAndPreviewScroll, 20)); + this._initInputPreviewSync($input, $preview); // Focus on the body unless we have a title if (!this.get('composer.canEditTitle') && !this.capabilities.isIOS) { @@ -134,29 +127,159 @@ export default Ember.Component.extend({ } }, - _syncEditorAndPreviewScroll() { - const $input = this.$('.d-editor-input'); - if (!$input) { return; } + _resetShouldBuildScrollMap() { + this.set('shouldBuildScrollMap', true); + }, - const $preview = this.$('.d-editor-preview'); + _initInputPreviewSync($input, $preview) { + REBUILD_SCROLL_MAP_EVENTS.forEach(event => { + this.appEvents.on(event, this, this._resetShouldBuildScrollMap); + }); - if ($input.scrollTop() === 0) { - $preview.scrollTop(0); - return; + Ember.run.scheduleOnce("afterRender", () => { + $input.on('touchstart mouseenter', () => { + if (!$preview.is(":visible")) return; + $preview.off('scroll'); + + $input.on('scroll', () => { + this._syncScroll(this._syncEditorAndPreviewScroll, $input, $preview); + }); + }); + + $preview.on('touchstart mouseenter', () => { + $input.off('scroll'); + + $preview.on('scroll', () => { + this._syncScroll(this._syncPreviewAndEditorScroll, $input, $preview); + }); + }); + }); + }, + + _syncScroll($callback, $input, $preview) { + if (!this.get('scrollMap') || this.get('shouldBuildScrollMap')) { + this.set('scrollMap', this._buildScrollMap($input, $preview)); + this.set('shouldBuildScrollMap', false); } - const inputHeight = $input[0].scrollHeight; - const previewHeight = $preview[0].scrollHeight; - if (($input.height() + $input.scrollTop() + 100) > inputHeight) { - // cheat, special case for bottom - $preview.scrollTop(previewHeight); - return; + Ember.run.throttle(this, $callback, $input, $preview, this.get('scrollMap'), 20); + }, + + _teardownInputPreviewSync() { + [this.$('.d-editor-input'), this.$('.d-editor-preview')].forEach($element => { + $element.off("mouseenter touchstart"); + $element.off("scroll"); + }); + + REBUILD_SCROLL_MAP_EVENTS.forEach(event => { + this.appEvents.off(event, this, this._resetShouldBuildScrollMap); + });; + }, + + // Adapted from https://github.com/markdown-it/markdown-it.github.io + _buildScrollMap($input, $preview) { + let sourceLikeDiv = $('
').css({ + position: 'absolute', + height: 'auto', + visibility: 'hidden', + width: $input[0].clientWidth, + 'font-size': $input.css('font-size'), + 'font-family': $input.css('font-family'), + 'line-height': $input.css('line-height'), + 'white-space': $input.css('white-space') + }).appendTo('body'); + + const linesMap = []; + let numberOfLines = 0; + + $input.val().split('\n').forEach(text => { + linesMap.push(numberOfLines); + + if (text.length === 0) { + numberOfLines++; + } else { + sourceLikeDiv.text(text); + + let height; + let lineHeight; + height = parseFloat(sourceLikeDiv.css('height')); + lineHeight = parseFloat(sourceLikeDiv.css('line-height')); + numberOfLines += Math.round(height / lineHeight); + } + }); + + linesMap.push(numberOfLines); + sourceLikeDiv.remove(); + + const previewOffsetTop = $preview.offset().top; + const offset = $preview.scrollTop() - previewOffsetTop - ($input.offset().top - previewOffsetTop); + const nonEmptyList = []; + const scrollMap = []; + for (let i = 0; i < numberOfLines; i++) { scrollMap.push(-1); }; + + nonEmptyList.push(0); + scrollMap[0] = 0; + + $preview.find('.preview-sync-line').each((_, element) => { + let $element = $(element); + let lineNumber = $element.data('line-number'); + let linesToTop = linesMap[lineNumber]; + if (linesToTop !== 0) { nonEmptyList.push(linesToTop); } + scrollMap[linesToTop] = Math.round($element.offset().top + offset); + }); + + nonEmptyList.push(numberOfLines); + scrollMap[numberOfLines] = $preview[0].scrollHeight; + + let position = 0; + + for (let i = 1; i < numberOfLines; i++) { + if (scrollMap[i] !== -1) { + position++; + continue; + } + + let top = nonEmptyList[position]; + let bottom = nonEmptyList[position + 1]; + + scrollMap[i] = + (( + scrollMap[bottom] * (i - top) + + scrollMap[top] * (bottom - i) + ) / (bottom - top)).toFixed(2); + }; + + return scrollMap; + }, + + _syncEditorAndPreviewScroll($input, $preview, scrollMap) { + let scrollTop; + + if (($input.height() + $input.scrollTop() + 100) > $input[0].scrollHeight) { + scrollTop = $preview[0].scrollHeight; + } else { + const lineHeight = parseFloat($input.css('line-height')); + const lineNumber = Math.floor($input.scrollTop() / lineHeight); + scrollTop = scrollMap[lineNumber]; } - const scrollPosition = $input.scrollTop(); - const factor = previewHeight / inputHeight; - const desired = scrollPosition * factor; - $preview.scrollTop(desired + 50); + $preview.stop(true).animate({ scrollTop }, 100, 'linear'); + }, + + _syncPreviewAndEditorScroll($input, $preview, scrollMap) { + if (scrollMap.length < 1) return; + + let scrollTop; + const previewScrollTop = $preview.scrollTop(); + + if (($preview.height() + previewScrollTop + 100) > $preview[0].scrollHeight) { + scrollTop = $input[0].scrollHeight; + } else { + const lineHeight = parseFloat($input.css('line-height')); + scrollTop = lineHeight * scrollMap.findIndex(offset => offset > previewScrollTop); + } + + $input.stop(true).animate({ scrollTop }, 100, 'linear'); }, _renderUnseenMentions($preview, unseen) { @@ -198,24 +321,6 @@ export default Ember.Component.extend({ $oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id)); }, - _loadShortUrls($images) { - const urls = _.map($images, img => $(img).data('orig-src')); - lookupUncachedUploadUrls(urls, ajax).then(() => this._loadCachedShortUrls($images)); - }, - - _loadCachedShortUrls($images) { - $images.each((idx, image) => { - let $image = $(image); - let url = lookupCachedUploadUrl($image.data('orig-src')); - if (url) { - $image.removeAttr('data-orig-src'); - if (url !== "missing") { - $image.attr('src', url); - } - } - }); - }, - _warnMentionedGroups($preview) { Ember.run.scheduleOnce('afterRender', () => { var found = this.get('warnedGroupMentions') || []; @@ -321,6 +426,19 @@ export default Ember.Component.extend({ } }); + $element.on("fileuploaddone", (e, data) => { + let upload = data.result; + + if (!this._xhr || !this._xhr._userCancelled) { + const markdown = getUploadMarkdown(upload); + cacheShortUploadUrl(upload.short_url, upload.url); + this.appEvents.trigger('composer:replace-text', uploadPlaceholder, markdown); + this._resetUpload(false); + } else { + this._resetUpload(true); + } + }); + $element.on("fileuploadfail", (e, data) => { this._resetUpload(true); @@ -328,29 +446,12 @@ export default Ember.Component.extend({ this._xhr = null; if (!userCancelled) { - displayErrorForUpload(data); - } - }); - - this.messageBus.subscribe("/uploads/composer", upload => { - // replace upload placeholder - if (upload && upload.url) { - if (!this._xhr || !this._xhr._userCancelled) { - const markdown = getUploadMarkdown(upload); - cacheShortUploadUrl(upload.short_url, upload.url); - this.appEvents.trigger('composer:replace-text', uploadPlaceholder, markdown); - this._resetUpload(false); - } else { - this._resetUpload(true); - } - } else { - this._resetUpload(true); - displayErrorForUpload(upload); + displayErrorForUpload(data.jqXHR.responseJSON); } }); if (this.site.mobileView) { - this.$(".mobile-file-upload").on("click.uploader", function () { + $("#reply-control .mobile-file-upload").on("click.uploader", function () { // redirect the click on the hidden file input $("#mobile-uploader").click(); }); @@ -360,29 +461,28 @@ export default Ember.Component.extend({ }, _optionsLocation() { - // long term we want some smart positioning algorithm in popup-menu - // the problem is that positioning in a fixed panel is a nightmare - // cause offsetParent can end up returning a fixed element and then - // using offset() is not going to work, so you end up needing special logic - // especially since we allow for negative .top, provided there is room on screen - const myPos = this.$().position(); - const buttonPos = this.$('.options').position(); + const composer = $("#reply-control"); + const composerOffset = composer.offset(); + const composerPosition = composer.position(); - const popupHeight = $('#reply-control .popup-menu').height(); - const popupWidth = $('#reply-control .popup-menu').width(); + const buttonBarOffset = $('#reply-control .d-editor-button-bar').offset(); + const optionsButton = $('#reply-control .d-editor-button-bar .options'); - var top = myPos.top + buttonPos.top - 15; - var left = myPos.left + buttonPos.left - (popupWidth/2); + const popupMenu = $("#reply-control .popup-menu"); + const popupWidth = popupMenu.outerWidth(); + const popupHeight = popupMenu.outerHeight(); - const composerPos = $('#reply-control').position(); + const headerHeight = $(".d-header").outerHeight(); - if (composerPos.top + top - popupHeight < 0) { - top = top + popupHeight + this.$('.options').height() + 50; + let left = optionsButton.offset().left - composerOffset.left; + let top = buttonBarOffset.top - composerOffset.top - popupHeight + popupMenu.innerHeight(); + + if (top + composerPosition.top - headerHeight - popupHeight < 0) { + top += popupHeight + optionsButton.outerHeight(); } - var replyWidth = $('#reply-control').width(); - if (left + popupWidth > replyWidth) { - left = replyWidth - popupWidth - 40; + if (left + popupWidth > composer.width()) { + left -= popupWidth - optionsButton.outerWidth(); } return { position: "absolute", left, top }; @@ -480,7 +580,7 @@ export default Ember.Component.extend({ @on('willDestroyElement') _unbindUploadTarget() { this._validUploads = 0; - this.$(".mobile-file-upload").off("click.uploader"); + $("#reply-control .mobile-file-upload").off("click.uploader"); this.messageBus.unsubscribe("/uploads/composer"); const $uploadTarget = this.$(); try { $uploadTarget.fileupload("destroy"); } @@ -491,14 +591,14 @@ export default Ember.Component.extend({ @on('willDestroyElement') _composerClosed() { this.appEvents.trigger('composer:will-close'); - this.appEvents.off('composer:show-preview'); - this.appEvents.off('composer:hide-preview'); Ember.run.next(() => { $('#main-outlet').css('padding-bottom', 0); // need to wait a bit for the "slide down" transition of the composer Ember.run.later(() => this.appEvents.trigger("composer:closed"), 400); }); + this._teardownInputPreviewSync(); + if (this.site.mobileView) { $(window).off('resize.composer-popup-menu'); } @@ -528,12 +628,12 @@ export default Ember.Component.extend({ } }, - showUploadModal(toolbarEvent) { - this.sendAction('showUploadSelector', toolbarEvent); + togglePreview() { + this.sendAction('togglePreview'); }, - togglePreview() { - this.toggleProperty('showPreview'); + showUploadModal(toolbarEvent) { + this.sendAction('showUploadSelector', toolbarEvent); }, extraButtons(toolbar) { @@ -605,18 +705,8 @@ export default Ember.Component.extend({ Ember.run.debounce(this, this._loadOneboxes, $oneboxes, 450); } - // Short upload urls - let $shortUploadUrls = $('img[data-orig-src]'); - - if ($shortUploadUrls.length > 0) { - this._loadCachedShortUrls($shortUploadUrls); - - $shortUploadUrls = $('img[data-orig-src]'); - if ($shortUploadUrls.length > 0) { - // this is carefully batched so we can do an leading debounce (trigger right away) - Ember.run.debounce(this, this._loadShortUrls, $shortUploadUrls, 450, true); - } - } + // Short upload urls need resolution + resolveAllShortUrls(ajax); let inline = {}; $('a.inline-onebox-loading', $preview).each(function(index, link) { @@ -630,6 +720,7 @@ export default Ember.Component.extend({ Ember.run.debounce(this, this._loadInlineOneboxes, inline, 450); } + this._syncScroll(this._syncEditorAndPreviewScroll, this.$('.d-editor-input'), $preview); this.trigger('previewRefreshed', $preview); this.sendAction('afterRefresh', $preview); }, diff --git a/app/assets/javascripts/discourse/components/composer-messages.js.es6 b/app/assets/javascripts/discourse/components/composer-messages.js.es6 index 3f5336c208..6f791315ab 100644 --- a/app/assets/javascripts/discourse/components/composer-messages.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-messages.js.es6 @@ -165,7 +165,6 @@ export default Ember.Component.extend({ if (topicId) { args.topic_id = topicId; } if (postId) { args.post_id = postId; } - const queuedForTyping = this.get('queuedForTyping'); composer.store.find('composer-message', args).then(messages => { if (this.isDestroying || this.isDestroyed) { return; } @@ -176,6 +175,7 @@ export default Ember.Component.extend({ } this.set('checkedMessages', true); + const queuedForTyping = this.get('queuedForTyping'); messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : this.send('popup', msg)); }); } diff --git a/app/assets/javascripts/discourse/components/composer-toggles.js.es6 b/app/assets/javascripts/discourse/components/composer-toggles.js.es6 index cd5369638c..205044922e 100644 --- a/app/assets/javascripts/discourse/components/composer-toggles.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-toggles.js.es6 @@ -3,6 +3,14 @@ import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ tagName: '', + @computed('composeState') + title(composeState) { + if (composeState === "draft" || composeState === "saving") { + return "composer.abandon"; + } + return "composer.collapse"; + }, + @computed('composeState') toggleIcon(composeState) { if (composeState === "draft" || composeState === "saving") { diff --git a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 index c248ea5b95..58bddf795a 100644 --- a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 +++ b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 @@ -1,7 +1,7 @@ import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ - classNameBindings: ['containerClass', 'condition:visible'], + classNameBindings: [':loading-container', 'containerClass', 'condition:visible'], @computed('size') containerClass(size) { diff --git a/app/assets/javascripts/discourse/components/cook-text.js.es6 b/app/assets/javascripts/discourse/components/cook-text.js.es6 index 80ed693563..60a96295c8 100644 --- a/app/assets/javascripts/discourse/components/cook-text.js.es6 +++ b/app/assets/javascripts/discourse/components/cook-text.js.es6 @@ -1,4 +1,5 @@ import { cookAsync } from 'discourse/lib/text'; +import { ajax } from 'discourse/lib/ajax'; const CookText = Ember.Component.extend({ tagName: '', @@ -6,7 +7,16 @@ const CookText = Ember.Component.extend({ didReceiveAttrs() { this._super(...arguments); - cookAsync(this.get('rawText')).then(cooked => this.set('cooked', cooked)); + cookAsync(this.get('rawText')).then( + cooked => { + this.set('cooked', cooked); + // no choice but to defer this cause + // pretty text may only be loaded now + Em.run.next(() => + window.requireModule('pretty-text/image-short-url').resolveAllShortUrls(ajax) + ); + } + ); } }); diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 313bfea7a4..ee6725c9cc 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -438,7 +438,9 @@ export default Ember.Component.extend({ } if (operation !== OP.ADDED && - (l.slice(0, hlen) === hval && tlen === 0 || l.slice(-tlen) === tail)) { + (l.slice(0, hlen) === hval && tlen === 0 || + (tail.length && l.slice(-tlen) === tail))) { + operation = OP.REMOVED; if (tlen === 0) { const result = l.slice(hlen); @@ -500,6 +502,7 @@ export default Ember.Component.extend({ tlen, opts ); + this.set('value', `${pre}${contents}${post}`); if (lines.length === 1 && tlen > 0) { this._selectText(sel.start + hlen, sel.value.length); diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 new file mode 100644 index 0000000000..ff1e6ef40a --- /dev/null +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -0,0 +1,25 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + tagName: '', + + @computed('category') + showCategoryNotifications(category) { + return category && this.currentUser; + }, + + @computed() + categories() { + return this.site.get('categoriesList'); + }, + + @computed('category.can_edit') + showCategoryEdit: canEdit => canEdit, + + @computed("filterMode", "category", 'noSubcategories') + navItems(filterMode, category, noSubcategories) { + // we don't want to show the period in the navigation bar since it's in a dropdown + if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); } + return Discourse.NavItem.buildList(category, { filterMode, noSubcategories }); + } +}); diff --git a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 index e26c24a21e..906acf2ae8 100644 --- a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 @@ -2,56 +2,69 @@ import DiscourseURL from 'discourse/lib/url'; import { buildCategoryPanel } from 'discourse/components/edit-category-panel'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import Category from 'discourse/models/category'; +import computed from 'ember-addons/ember-computed-decorators'; export default buildCategoryPanel('general', { foregroundColors: ['FFFFFF', '000000'], canSelectParentCategory: Em.computed.not('category.isUncategorizedCategory'), // background colors are available as a pipe-separated string - backgroundColors: function() { - const categories = Discourse.Category.list(); + @computed + backgroundColors() { + const categories = this.site.get('categoriesList'); return this.siteSettings.category_colors.split("|").map(function(i) { return i.toUpperCase(); }).concat( categories.map(function(c) { return c.color.toUpperCase(); }) ).uniq(); - }.property(), + }, - usedBackgroundColors: function() { - const categories = Discourse.Category.list(); - const category = this.get('category'); + @computed + noCategoryStyle() { + return this.siteSettings.category_style === 'none'; + }, + + @computed('category.id', 'category.color') + usedBackgroundColors(categoryId, categoryColor) { + const categories = this.site.get('categoriesList'); // If editing a category, don't include its color: return categories.map(function(c) { - return (category.get('id') && category.get('color').toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase(); + return (categoryId && categoryColor.toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase(); }, this).compact(); - }.property('category.id', 'category.color'), + }, - parentCategories: function() { - return Discourse.Category.list().filter(function (c) { - return !c.get('parentCategory'); - }); - }.property(), + @computed + parentCategories() { + return this.site.get('categoriesList').filter(c => !c.get('parentCategory')); + }, - categoryBadgePreview: function() { + @computed( + 'category.parent_category_id', + 'category.categoryName', + 'category.color', + 'category.text_color' + ) + categoryBadgePreview(parentCategoryId, name, color, textColor) { const category = this.get('category'); const c = Category.create({ - name: category.get('categoryName'), - color: category.get('color'), - text_color: category.get('text_color'), - parent_category_id: parseInt(category.get('parent_category_id'),10), + name, + color, + text_color: textColor, + parent_category_id: parseInt(parentCategoryId), read_restricted: category.get('read_restricted') }); - return categoryBadgeHTML(c, {link: false}); - }.property('category.parent_category_id', 'category.categoryName', 'category.color', 'category.text_color'), - + return categoryBadgeHTML(c, { link: false }); + }, // We can change the parent if there are no children - subCategories: function() { - if (Ember.isEmpty(this.get('category.id'))) { return null; } - return Category.list().filterBy('parent_category_id', this.get('category.id')); - }.property('category.id'), + @computed('category.id') + subCategories(categoryId) { + if (Ember.isEmpty(categoryId)) { return null; } + return Category.list().filterBy('parent_category_id', categoryId); + }, - showDescription: function() { - return !this.get('category.isUncategorizedCategory') && this.get('category.id'); - }.property('category.isUncategorizedCategory', 'category.id'), + @computed('category.isUncategorizedCategory', 'category.id') + showDescription(isUncategorizedCategory, categoryId) { + return !isUncategorizedCategory && categoryId; + }, actions: { showCategoryTopic() { diff --git a/app/assets/javascripts/discourse/components/emoji-picker.js.es6 b/app/assets/javascripts/discourse/components/emoji-picker.js.es6 index ce75e4fc7e..03efe27a53 100644 --- a/app/assets/javascripts/discourse/components/emoji-picker.js.es6 +++ b/app/assets/javascripts/discourse/components/emoji-picker.js.es6 @@ -474,9 +474,8 @@ export default Ember.Component.extend({ desktopModalePositioning(); } else { let previewInputOffset = $(".d-editor-input").offset(); - let replyControlOffset = $("#reply-control").offset() || {left: 0}; - let left = previewInputOffset.left - replyControlOffset.left; - desktopPositioning({left, bottom: $("#reply-control").height() - 48}); + let left = previewInputOffset.left; + desktopPositioning({left, bottom: $("#reply-control").height() - 45}); } } } diff --git a/app/assets/javascripts/discourse/components/expand-post.js.es6 b/app/assets/javascripts/discourse/components/expand-post.js.es6 index 957750e92f..643781dba4 100644 --- a/app/assets/javascripts/discourse/components/expand-post.js.es6 +++ b/app/assets/javascripts/discourse/components/expand-post.js.es6 @@ -2,17 +2,28 @@ import { ajax } from 'discourse/lib/ajax'; export default Ember.Component.extend({ tagName: '', + expanded: null, + _loading: false, actions: { - expandItem() { + toggleItem() { + if (this._loading) { return false; } const item = this.get('item'); + + if (this.get('expanded')) { + this.set('expanded', false); + item.set('expandedExcerpt', null); + return; + } + const topicId = item.get('topic_id'); const postNumber = item.get('post_number'); + this._loading = true; return ajax(`/posts/by_number/${topicId}/${postNumber}.json`).then(result => { - item.set('truncated', false); - item.set('excerpt', result.cooked); - }); + this.set('expanded', true); + item.set('expandedExcerpt', result.cooked); + }).finally(() => this._loading = false); } } }); diff --git a/app/assets/javascripts/discourse/components/future-date-input.js.es6 b/app/assets/javascripts/discourse/components/future-date-input.js.es6 index 97b8738787..5c9d37527e 100644 --- a/app/assets/javascripts/discourse/components/future-date-input.js.es6 +++ b/app/assets/javascripts/discourse/components/future-date-input.js.es6 @@ -1,7 +1,7 @@ import { default as computed, observes } from "ember-addons/ember-computed-decorators"; import { FORMAT, -} from "select-box-kit/components/future-date-input-selector"; +} from "select-kit/components/future-date-input-selector"; import { PUBLISH_TO_CATEGORY_STATUS_TYPE } from 'discourse/controllers/edit-topic-timer'; diff --git a/app/assets/javascripts/discourse/components/group-activity-filter.js.es6 b/app/assets/javascripts/discourse/components/group-activity-filter.js.es6 new file mode 100644 index 0000000000..d100e27e25 --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-activity-filter.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: 'li' +}); diff --git a/app/assets/javascripts/discourse/components/group-navigation.js.es6 b/app/assets/javascripts/discourse/components/group-navigation.js.es6 new file mode 100644 index 0000000000..35f5f238e4 --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-navigation.js.es6 @@ -0,0 +1,15 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + tagName: '', + + @computed('group') + availableTabs(group) { + return this.get('tabs').filter(t => { + if (t.admin) { + return this.currentUser ? this.currentUser.canManageGroup(group) : false; + } + return true; + }); + } +}); diff --git a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 index 5f9e1266f1..2487a0d50d 100644 --- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 @@ -5,7 +5,7 @@ import { renderedConnectorsFor } from 'discourse/lib/plugin-connectors'; export default Ember.Component.extend({ tagName: 'ul', classNameBindings: [':nav', ':nav-pills'], - id: 'navigation-bar', + elementId: 'navigation-bar', init() { this._super(); diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6 index 9ee2e38905..888eb3c28c 100644 --- a/app/assets/javascripts/discourse/components/navigation-item.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6 @@ -16,7 +16,18 @@ export default Ember.Component.extend(bufferedRender({ buildBuffer(buffer) { const content = this.get('content'); - buffer.push(""); + + let href = content.get('href'); + + // Include the category id if the option is present + if (content.get('includeCategoryId')) { + let categoryId = this.get('category.id'); + if (categoryId) { + href += `?category_id=${categoryId}`; + } + } + + buffer.push(``); if (content.get('hasIcon')) { buffer.push(""); } diff --git a/app/assets/javascripts/discourse/components/popup-menu.js.es6 b/app/assets/javascripts/discourse/components/popup-menu.js.es6 index 4f8de9b592..64ef71752d 100644 --- a/app/assets/javascripts/discourse/components/popup-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/popup-menu.js.es6 @@ -11,11 +11,8 @@ export default Ember.Component.extend({ this.sendAction('hide'); }); - $('html').on(`mouseup.popup-menu-${this.get('elementId')}`, (e) => { - const $target = $(e.target); - if ($target.is("button") || this.$().has($target).length === 0) { - this.sendAction('hide'); - } + $('html').on(`mouseup.popup-menu-${this.get('elementId')}`, () => { + this.sendAction('hide'); }); }, diff --git a/app/assets/javascripts/discourse/components/quote-button.js.es6 b/app/assets/javascripts/discourse/components/quote-button.js.es6 index 4d28b05761..5d41b049cc 100644 --- a/app/assets/javascripts/discourse/components/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/components/quote-button.js.es6 @@ -40,9 +40,18 @@ export default Ember.Component.extend({ } } - quoteState.selected(postId, selectedText()); + const _selectedText = selectedText(); + quoteState.selected(postId, _selectedText); this.set('visible', quoteState.buffer.length > 0); + // avoid hard loops in quote selection unconditionally + // this can happen if you triple click text in firefox + if (this._prevSelection === _selectedText) { + return; + } + + this._prevSelection = _selectedText; + // on Desktop, shows the button at the beginning of the selection // on Mobile, shows the button at the end of the selection const isMobileDevice = this.site.isMobileDevice; @@ -101,12 +110,14 @@ export default Ember.Component.extend({ const onSelectionChanged = _.debounce(() => this._selectionChanged(), wait); $(document).on("mousedown.quote-button", e => { + this._prevSelection = null; this._isMouseDown = true; this._reselected = false; if ($(e.target).closest('.quote-button, .create, .share, .reply-new').length === 0) { this._hideButton(); } }).on("mouseup.quote-button", () => { + this._prevSelection = null; this._isMouseDown = false; onSelectionChanged(); }).on("selectionchange.quote-button", () => { diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 index 87e59f60ba..2192c4e3d8 100644 --- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 +++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 @@ -4,13 +4,13 @@ import { cloak, uncloak } from 'discourse/widgets/post-stream'; import { isWorkaroundActive } from 'discourse/lib/safari-hacks'; import offsetCalculator from 'discourse/lib/offset-calculator'; -function findTopView($posts, viewportTop, min, max) { +function findTopView($posts, viewportTop, postsWrapperTop, min, max) { if (max < min) { return min; } while (max > min) { const mid = Math.floor((min + max) / 2); const $post = $($posts[mid]); - const viewBottom = $post.position().top + $post.height(); + const viewBottom = ($post.offset().top - postsWrapperTop) + $post.height(); if (viewBottom > viewportTop) { max = mid-1; @@ -63,6 +63,10 @@ export default MountWidget.extend({ if (this.isDestroyed || this.isDestroying) { return; } if (isWorkaroundActive()) { return; } + // We use this because watching videos fullscreen in Chrome was super buggy + // otherwise. Thanks to arrendek from q23 for the technique. + if (document.elementFromPoint(0, 0).tagName.toUpperCase() === "IFRAME") { return; } + const $w = $(window); const windowHeight = window.innerHeight ? window.innerHeight : $w.height(); const slack = Math.round(windowHeight * 5); @@ -71,9 +75,10 @@ export default MountWidget.extend({ const windowTop = $w.scrollTop(); + const postsWrapperTop = $('.posts-wrapper').offset().top; const $posts = this.$('.onscreen-post, .cloaked-post'); const viewportTop = windowTop - slack; - const topView = findTopView($posts, viewportTop, 0, $posts.length-1); + const topView = findTopView($posts, viewportTop, postsWrapperTop, 0, $posts.length-1); let windowBottom = windowTop + windowHeight; let viewportBottom = windowBottom + slack; diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 index f29c6f95a3..a171083523 100644 --- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 +++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 @@ -185,18 +185,18 @@ export default Em.Component.extend({ const userInput = Discourse.Category.findBySlug(subcategories[1], subcategories[0]); if ((!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id)) - this.set('searchedTerms.category', [userInput]); + this.set('searchedTerms.category', userInput); } else if (isNaN(subcategories)) { const userInput = Discourse.Category.findSingleBySlug(subcategories[0]); if ((!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id)) - this.set('searchedTerms.category', [userInput]); + this.set('searchedTerms.category', userInput); } else { const userInput = Discourse.Category.findById(subcategories[0]); if ((!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id)) - this.set('searchedTerms.category', [userInput]); + this.set('searchedTerms.category', userInput); } } else this.set('searchedTerms.category', ''); @@ -303,11 +303,11 @@ export default Em.Component.extend({ const slugCategoryMatches = (match.length !== 0) ? match[0].match(REGEXP_CATEGORY_SLUG) : null; const idCategoryMatches = (match.length !== 0) ? match[0].match(REGEXP_CATEGORY_ID) : null; - if (categoryFilter && categoryFilter[0]) { - const id = categoryFilter[0].id; - const slug = categoryFilter[0].slug; - if (categoryFilter[0].parentCategory) { - const parentSlug = categoryFilter[0].parentCategory.slug; + if (categoryFilter) { + const id = categoryFilter.id; + const slug = categoryFilter.slug; + if (categoryFilter.parentCategory) { + const parentSlug = categoryFilter.parentCategory.slug; if (slugCategoryMatches) searchTerm = searchTerm.replace(slugCategoryMatches[0], `#${parentSlug}:${slug}`); else if (idCategoryMatches) diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 index cfa9340e94..8512ca6794 100644 --- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 @@ -12,6 +12,7 @@ export default MountWidget.extend(Docking, { buildArgs() { let attrs = { topic: this.get('topic'), + notificationLevel: this.get('notificationLevel'), topicTrackingState: this.topicTrackingState, enteredIndex: this.get('enteredIndex'), dockAt: this.dockAt, diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 index c721591563..238b86cdb2 100644 --- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 +++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 @@ -6,12 +6,14 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor import DiscourseURL from 'discourse/lib/url'; import User from 'discourse/models/user'; import { userPath } from 'discourse/lib/url'; +import { durationTiny } from 'discourse/lib/formatter'; +import CanCheckEmails from 'discourse/mixins/can-check-emails'; const clickOutsideEventName = "mousedown.outside-user-card"; const clickDataExpand = "click.discourse-user-card"; const clickMention = "click.discourse-user-mention"; -export default Ember.Component.extend(CleansUp, { +export default Ember.Component.extend(CleansUp, CanCheckEmails, { elementId: 'user-card', classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg'], allowBackgrounds: setting('allow_profile_backgrounds'), @@ -29,6 +31,7 @@ export default Ember.Component.extend(CleansUp, { showDelete: Ember.computed.and("viewingAdmin", "showName", "user.canBeDeleted"), linkWebsite: Ember.computed.not('user.isBasic'), hasLocationOrWebsite: Ember.computed.or('user.location', 'user.website_name'), + showCheckEmail: Ember.computed.and('user.staged', 'canCheckEmails'), visible: false, user: null, @@ -87,6 +90,25 @@ export default Ember.Component.extend(CleansUp, { $this.css('background-image', bg); }, + @computed('user.time_read', 'user.recent_time_read') + showRecentTimeRead(timeRead, recentTimeRead) { + return timeRead !== recentTimeRead && recentTimeRead !== 0; + }, + + @computed('user.recent_time_read') + recentTimeRead(recentTimeReadSeconds) { + return durationTiny(recentTimeReadSeconds); + }, + + @computed('showRecentTimeRead', 'user.time_read', 'recentTimeRead') + timeReadTooltip(showRecent, timeRead, recentTimeRead) { + if (showRecent) { + return I18n.t('time_read_recently_tooltip', {time_read: durationTiny(timeRead), recent_time_read: recentTimeRead}); + } else { + return I18n.t('time_read_tooltip', {time_read: durationTiny(timeRead)}); + } + }, + _show(username, $target) { // No user card for anon if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { @@ -271,6 +293,10 @@ export default Ember.Component.extend(CleansUp, { showUser() { this.sendAction('showUser', this.get('user')); this._close(); + }, + + checkEmail(user) { + user.checkEmail(); } } }); diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index fd9bb6ba3b..830a74d708 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -42,7 +42,7 @@ export default TextField.extend({ allowAny: this.get('allowAny'), updateData: (opts && opts.updateData) ? opts.updateData : false, - dataSource: function(term) { + dataSource(term) { const termRegex = Discourse.User.currentProp('can_send_private_email_messages') ? /[^a-zA-Z0-9_\-\.@\+]/ : /[^a-zA-Z0-9_\-\.]/; @@ -60,7 +60,7 @@ export default TextField.extend({ return results; }, - transformComplete: function(v) { + transformComplete(v) { if (v.username || v.name) { if (!v.username) { groups.push(v.name); } return v.username || v.name; @@ -72,7 +72,7 @@ export default TextField.extend({ } }, - onChangeItems: function(items) { + onChangeItems(items) { var hasGroups = false; items = items.map(function(i) { if (groups.indexOf(i) > -1) { hasGroups = true; } @@ -85,7 +85,7 @@ export default TextField.extend({ if (self.get('onChangeCallback')) self.sendAction('onChangeCallback'); }, - reverseTransform: function(i) { + reverseTransform(i) { return { username: i }; } diff --git a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 index ab30fef157..170f28fc30 100644 --- a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 @@ -36,10 +36,17 @@ export default Ember.Controller.extend(ModalFunctionality, { refreshGravatar() { this.set("gravatarRefreshDisabled", true); return ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" }) - .then(result => this.setProperties({ - gravatar_avatar_template: result.gravatar_avatar_template, - gravatar_avatar_upload_id: result.gravatar_upload_id, - })) + .then(result => { + if (!result.gravatar_avatar_upload_id) { + this.set("gravatarFailed", true); + } else { + this.setProperties({ + gravatarFailed: false, + gravatar_avatar_template: result.gravatar_avatar_template, + gravatar_avatar_upload_id: result.gravatar_upload_id, + }); + } + }) .finally(() => this.set("gravatarRefreshDisabled", false)); } } diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 61f381a239..b9ae01d5f4 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -2,7 +2,7 @@ import DiscourseURL from 'discourse/lib/url'; import Quote from 'discourse/lib/quote'; import Draft from 'discourse/models/draft'; import Composer from 'discourse/models/composer'; -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators'; import InputValidation from 'discourse/models/input-validation'; import { getOwner } from 'discourse-common/lib/get-owner'; import { escapeExpression } from 'discourse/lib/utilities'; @@ -68,7 +68,28 @@ export default Ember.Controller.extend({ isUploading: false, topic: null, linkLookup: null, + showPreview: true, + forcePreview: Ember.computed.and('site.mobileView', 'showPreview'), whisperOrUnlistTopic: Ember.computed.or('model.whisper', 'model.unlistTopic'), + categories: Ember.computed.alias('site.categoriesList'), + + @on('init') + _setupPreview() { + const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true')); + this.set('showPreview', val === 'true'); + }, + + @computed('showPreview') + toggleText: function(showPreview) { + return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview'); + }, + + @observes('showPreview') + showPreviewChanged() { + if (!this.site.mobileView) { + this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') }); + } + }, @computed('model.replyingToTopic', 'model.creatingPrivateMessage', 'model.targetUsernames') focusTarget(replyingToTopic, creatingPM, usernames) { @@ -205,6 +226,10 @@ export default Ember.Controller.extend({ actions: { + togglePreview() { + this.toggleProperty('showPreview'); + }, + typed() { this.checkReplyLength(); this.get('model').typing(); @@ -278,20 +303,18 @@ export default Ember.Controller.extend({ // Toggle the reply view toggle() { this.closeAutocomplete(); - if (this.get('model.composeState') === Composer.OPEN) { - if (Ember.isEmpty(this.get('model.reply')) && Ember.isEmpty(this.get('model.title'))) { - this.close(); - } else { - this.shrink(); - } - } else { - this.close(); - } - return false; - }, - togglePreview() { - this.get('model').togglePreview(); + if (Ember.isEmpty(this.get('model.reply')) && Ember.isEmpty(this.get('model.title'))) { + this.close(); + } else { + if (this.get('model.composeState') === Composer.OPEN) { + this.shrink(); + } else { + this.cancelComposer(); + } + } + + return false; }, // Import a quote from the post @@ -367,8 +390,9 @@ export default Ember.Controller.extend({ const body = I18n.t('composer.group_mentioned', { group: "@" + group.name, count: group.user_count, - group_link: Discourse.getURL(`/group/${group.name}/members`) + group_link: Discourse.getURL(`/groups/${group.name}/members`) }); + this.appEvents.trigger('composer-messages:create', { extraClass: 'custom-body', templateName: 'custom-body', @@ -396,10 +420,6 @@ export default Ember.Controller.extend({ }, - categories: function() { - return Discourse.Category.list(); - }.property(), - disableSubmit: Ember.computed.or("model.loading", "isUploading"), save(force) { @@ -654,7 +674,7 @@ export default Ember.Controller.extend({ if (!splitCategory[1]) { category = this.site.get('categories').findBy('nameLower', splitCategory[0].toLowerCase()); } else { - const categories = Discourse.Category.list(); + const categories = this.site.get('categories'); const mainCategory = categories.findBy('nameLower', splitCategory[0].toLowerCase()); category = categories.find(function(item) { return item && item.get('nameLower') === splitCategory[1].toLowerCase() && item.get('parent_category_id') === mainCategory.id; @@ -720,7 +740,7 @@ export default Ember.Controller.extend({ }, shrink() { - if (this.get('model.replyDirty')) { + if (this.get('model.replyDirty') || (this.get('model.canEditTitle') && this.get('model.titleDirty'))) { this.collapse(); } else { this.close(); diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 index e05c37937b..2af5d3593d 100644 --- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 @@ -65,7 +65,7 @@ export default Ember.Controller.extend(ModalFunctionality, { saveCategory() { const self = this, model = this.get('model'), - parentCategory = Discourse.Category.list().findBy('id', parseInt(model.get('parent_category_id'), 10)); + parentCategory = this.site.get('categories').findBy('id', parseInt(model.get('parent_category_id'), 10)); this.set('saving', true); model.set('parentCategory', parentCategory); diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index ef661f1928..5fe60501c4 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -104,11 +104,13 @@ export default Ember.Controller.extend({ cleanTerm(term) { if (term) { SortOrders.forEach(order => { - let matches = term.match(new RegExp(`${order.term}\\b`)); - if (matches) { - this.set('sortOrder', order.id); - term = term.replace(new RegExp(`${order.term}\\b`, 'g'), ""); - term = term.trim(); + if (order.term) { + let matches = term.match(new RegExp(`${order.term}\\b`)); + if (matches) { + this.set('sortOrder', order.id); + term = term.replace(new RegExp(`${order.term}\\b`, 'g'), ""); + term = term.trim(); + } } }); } @@ -159,9 +161,9 @@ export default Ember.Controller.extend({ return this.currentUser && this.currentUser.staff && hasResults; }, - @computed('expanded') - canCreateTopic(expanded) { - return this.currentUser && !this.site.mobileView && !expanded; + @computed('expanded', 'model.grouped_search_result.can_create_topic') + canCreateTopic(expanded, userCanCreateTopic) { + return this.currentUser && userCanCreateTopic && !this.site.mobileView && !expanded; }, @computed('expanded') diff --git a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 index c2434c83c6..4047450a1c 100644 --- a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 @@ -1,12 +1,17 @@ +import { observes } from 'ember-addons/ember-computed-decorators'; import { fmt } from 'discourse/lib/computed'; export default Ember.Controller.extend({ group: Ember.inject.controller(), + groupActivity: Ember.inject.controller(), + application: Ember.inject.controller(), + canLoadMore: true, loading: false, emptyText: fmt('type', 'groups.empty.%@'), actions: { loadMore() { + if (!this.get('canLoadMore')) { return; } if (this.get('loading')) { return; } this.set('loading', true); const posts = this.get('model'); @@ -14,12 +19,23 @@ export default Ember.Controller.extend({ const beforePostId = posts[posts.length-1].get('id'); const group = this.get('group.model'); - const opts = { beforePostId, type: this.get('type') }; + let categoryId = this.get('groupActivity.category_id'); + const opts = { beforePostId, type: this.get('type'), categoryId }; + group.findPosts(opts).then(newPosts => { posts.addObjects(newPosts); + if(newPosts.length === 0) { + this.set('canLoadMore', false); + } + }).finally(() => { this.set('loading', false); }); } } + }, + + @observes('canLoadMore') + _showFooter() { + this.set("application.showFooter", !this.get("canLoadMore")); } }); diff --git a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity.js.es6 index 955822ffcf..de4bda4dcd 100644 --- a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-activity.js.es6 @@ -2,6 +2,7 @@ import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend({ application: Ember.inject.controller(), + queryParams: ['category_id'], @computed('model.is_group_user') showGroupMessages(isGroupUser) { diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6 index 4734ac8db4..98e4687f89 100644 --- a/app/assets/javascripts/discourse/controllers/group.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group.js.es6 @@ -1,14 +1,11 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -var Tab = Em.Object.extend({ - @computed('name') - location(name) { - return 'group.' + name; - }, - - @computed('name', 'i18nKey') - message(name, i18nKey) { - return I18n.t(`groups.${i18nKey || name}`); +const Tab = Ember.Object.extend({ + init() { + this._super(); + let name = this.get('name'); + this.set('route', this.get('route') || `group.` + name); + this.set('message', I18n.t(`groups.${this.get('i18nKey') || name}`)); } }); @@ -18,13 +15,13 @@ export default Ember.Controller.extend({ showing: 'members', tabs: [ - Tab.create({ name: 'members', 'location': 'group.index', icon: 'users' }), + Tab.create({ name: 'members', route: 'group.index', icon: 'users' }), Tab.create({ name: 'activity' }), Tab.create({ - name: 'edit', i18nKey: 'edit.title', icon: 'pencil', requiresGroupAdmin: true + name: 'edit', i18nKey: 'edit.title', icon: 'pencil', admin: true }), Tab.create({ - name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', requiresGroupAdmin: true + name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', admin: true }) ], @@ -58,21 +55,6 @@ export default Ember.Controller.extend({ this.get('tabs')[0].set('count', this.get('model.user_count')); }, - @computed('model.is_group_owner', 'model.automatic') - getTabs() { - return this.get('tabs').filter(t => { - let canSee = true; - - if (this.currentUser && t.requiresGroupAdmin) { - canSee = this.currentUser.canManageGroup(this.get('model')); - } else if (t.requiresGroupAdmin) { - canSee = false; - } - - return canSee; - }); - }, - actions: { messageGroup() { this.send('createNewMessageViaParams', this.get('model.name')); diff --git a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 index 143812960b..0d757e4bce 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 @@ -1,12 +1,6 @@ -import computed from "ember-addons/ember-computed-decorators"; import NavigationDefaultController from 'discourse/controllers/navigation/default'; export default NavigationDefaultController.extend({ showingParentCategory: Em.computed.none('category.parentCategory'), showingSubcategoryList: Em.computed.and('category.show_subcategory_list', 'showingParentCategory'), - - @computed("showingSubcategoryList", "category", "noSubcategories") - navItems(showingSubcategoryList, category, noSubcategories) { - return Discourse.NavItem.buildList(category, { noSubcategories }); - } }); diff --git a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 index 93ae014e47..1fa8eedcc7 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 @@ -1,19 +1,4 @@ -import computed from "ember-addons/ember-computed-decorators"; - export default Ember.Controller.extend({ discovery: Ember.inject.controller(), discoveryTopics: Ember.inject.controller('discovery/topics'), - - @computed() - categories() { - return Discourse.Category.list(); - }, - - @computed("filterMode") - navItems(filterMode) { - // we don't want to show the period in the navigation bar since it's in a dropdown - if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); } - return Discourse.NavItem.buildList(null, { filterMode }); - } - }); diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 index e7694a2301..91c62436d6 100644 --- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 +++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 @@ -7,6 +7,7 @@ import { userPath } from 'discourse/lib/url'; export default Ember.Controller.extend(PasswordValidation, { isDeveloper: Ember.computed.alias('model.is_developer'), + admin: Ember.computed.alias('model.admin'), passwordRequired: true, errorMessage: null, successMessage: null, diff --git a/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 index f2c543e6be..c39fb86395 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 @@ -1,28 +1,9 @@ -import { ajax } from 'discourse/lib/ajax'; import BadgeSelectController from "discourse/mixins/badge-select-controller"; export default Ember.Controller.extend(BadgeSelectController, { filteredList: function() { return this.get('model').filterBy('badge.allow_title', true); - }.property('model'), + }.property('model') - actions: { - save() { - this.setProperties({ saved: false, saving: true }); - - ajax(this.get('user.path') + "/preferences/badge_title", { - type: "PUT", - data: { user_badge_id: this.get('selectedUserBadgeId') } - }).then(() => { - this.setProperties({ - saved: true, - saving: false, - "user.title": this.get('selectedUserBadge.badge.name') - }); - }, () => { - bootbox.alert(I18n.t('generic_error')); - }); - } - } }); diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 index 002eaba949..1b20fabe22 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 @@ -1,8 +1,11 @@ import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; +import { setDefaultHomepage } from "discourse/lib/utilities"; import { default as computed, observes } from "ember-addons/ember-computed-decorators"; import { currentThemeKey, listThemes, previewTheme, setLocalTheme } from 'discourse/lib/theme-selector'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +const USER_HOMES = { 1: "latest", 2: "categories", 3: "unread", 4: "new", 5: "top" }; + export default Ember.Controller.extend(PreferencesTabController, { @computed("makeThemeDefault") @@ -14,6 +17,8 @@ export default Ember.Controller.extend(PreferencesTabController, { 'enable_quoting', 'disable_jump_reply', 'automatically_unpin_topics', + 'allow_private_messages', + 'homepage_id', ]; if (makeDefault) { @@ -51,6 +56,19 @@ export default Ember.Controller.extend(PreferencesTabController, { previewTheme(key); }, + homeChanged() { + const siteHome = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; + const userHome = USER_HOMES[this.get('model.user_option.homepage_id')]; + setDefaultHomepage(userHome || siteHome); + }, + + @computed() + userSelectableHome() { + return _.map(USER_HOMES, (name, num) => { + return {name: I18n.t('filters.' + name + '.title'), value: Number(num)}; + }); + }, + actions: { save() { this.set('saved', false); @@ -66,6 +84,8 @@ export default Ember.Controller.extend(PreferencesTabController, { setLocalTheme(this.get('themeKey'), this.get('model.user_option.theme_key_seq')); } + this.homeChanged(); + }).catch(popupAjaxError); } } diff --git a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 index 467750926f..4658b09f38 100644 --- a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 @@ -58,6 +58,8 @@ export default Ember.Controller.extend(BulkTopicSelection, { max_posts: null, q: null, + categories: Ember.computed.alias('site.categoriesList'), + queryParams: ['order', 'ascending', 'status', 'state', 'search', 'max_posts', 'q'], navItems: function() { @@ -68,10 +70,6 @@ export default Ember.Controller.extend(BulkTopicSelection, { return Discourse.SiteSettings.show_filter_by_tag; }.property('category'), - categories: function() { - return Discourse.Category.list(); - }.property(), - showAdminControls: function() { return !this.get('additionalTags') && this.get('canAdminTag') && !this.get('category'); }.property('additionalTags', 'canAdminTag', 'category'), 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 4b2a19d457..997270dc5f 100644 --- a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 @@ -115,7 +115,6 @@ export default Ember.Controller.extend(ModalFunctionality, { showChangeCategory() { this.send('changeBulkTemplate', 'modal/bulk-change-category'); - this.set('modal.modalClass', 'topic-bulk-actions-modal full'); }, showNotificationLevel() { diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index fe2a3f4fb0..bc79faf6b0 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -12,6 +12,7 @@ import debounce from 'discourse/lib/debounce'; import isElementInViewport from "discourse/lib/is-element-in-viewport"; import QuoteState from 'discourse/lib/quote-state'; import { userPath } from 'discourse/lib/url'; +import { extractLinkMeta } from 'discourse/lib/render-topic-featured-link'; export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { composer: Ember.inject.controller(), @@ -32,6 +33,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { username_filters: null, filter: null, quoteState: null, + canRemoveTopicFeaturedLink: Ember.computed.and('canEditTopicFeaturedLink', 'buffered.featured_link'), updateQueryParams() { const postStream = this.get('model.postStream'); @@ -99,6 +101,12 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1; }, + @computed('model') + featuredLinkDomain(topic) { + const meta = extractLinkMeta(topic); + return meta.domain; + }, + @computed('model.isPrivateMessage') canEditTags(isPrivateMessage) { return !isPrivateMessage && this.site.get('can_tag_topics'); @@ -123,9 +131,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { const composer = this.get('composer'); const viewOpen = composer.get('model.viewOpen'); + const quotedText = Quote.build(post, buffer); + // If we can't create a post, delegate to reply as new topic if ((!viewOpen) && (!this.get('model.details.can_create_post'))) { - this.send('replyAsNewTopic', post); + this.send('replyAsNewTopic', post, quotedText); return; } @@ -146,7 +156,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { composerOpts.post = composerPost; } - const quotedText = Quote.build(post, buffer); composerOpts.quote = quotedText; if (composer.get('model.viewOpen')) { this.appEvents.trigger('composer:insert-block', quotedText); @@ -615,11 +624,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { } }, - replyAsNewTopic(post) { + replyAsNewTopic(post, quotedText) { const composerController = this.get('composer'); const { quoteState } = this; - const quotedText = Quote.build(post, quoteState.buffer); + quotedText = quotedText || Quote.build(post, quoteState.buffer); quoteState.clear(); var options; @@ -694,6 +703,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { convertToPrivateMessage() { this.get('content').convertTopic("private"); + }, + + removeFeaturedLink() { + this.set('buffered.featured_link', null); } }, @@ -752,9 +765,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { return selectedPostsUsername !== undefined; }, - categories: function() { - return Discourse.Category.list(); - }.property(), + categories: Ember.computed.alias('site.categoriesList'), canSelectAll: Em.computed.not('allPostsSelected'), diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 index 624a8b0ae7..24d4c33480 100644 --- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 @@ -3,11 +3,9 @@ import { exportUserArchive } from 'discourse/lib/export-csv'; export default Ember.Controller.extend({ application: Ember.inject.controller(), user: Ember.inject.controller(), - userActionType: null, - currentPath: Ember.computed.alias('application.currentPath'), - viewingSelf: Ember.computed.alias("user.viewingSelf"), - showBookmarks: Ember.computed.alias("user.showBookmarks"), + + canDownloadPosts: Ember.computed.alias('user.viewingSelf'), _showFooter: function() { var showFooter; @@ -26,11 +24,7 @@ export default Ember.Controller.extend({ I18n.t("user.download_archive.confirm"), I18n.t("no_value"), I18n.t("yes_value"), - function(confirmed) { - if (confirmed) { - exportUserArchive(); - } - } + confirmed => confirmed ? exportUserArchive() : null ); } } diff --git a/app/assets/javascripts/discourse/controllers/user-summary.js.es6 b/app/assets/javascripts/discourse/controllers/user-summary.js.es6 index bb4bec2077..d489f9ba28 100644 --- a/app/assets/javascripts/discourse/controllers/user-summary.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-summary.js.es6 @@ -1,4 +1,5 @@ import computed from 'ember-addons/ember-computed-decorators'; +import { durationTiny } from 'discourse/lib/formatter'; // should be kept in sync with 'UserSummary::MAX_BADGES' const MAX_BADGES = 6; @@ -9,4 +10,19 @@ export default Ember.Controller.extend({ @computed("model.badges.length") moreBadges(badgesLength) { return badgesLength >= MAX_BADGES; }, + + @computed('model.time_read') + timeRead(timeReadSeconds) { + return durationTiny(timeReadSeconds); + }, + + @computed('model.time_read', 'model.recent_time_read') + showRecentTimeRead(timeRead, recentTimeRead) { + return timeRead !== recentTimeRead && recentTimeRead !== 0; + }, + + @computed('model.recent_time_read') + recentTimeRead(recentTimeReadSeconds) { + return recentTimeReadSeconds > 0 ? durationTiny(recentTimeReadSeconds) : null; + } }); diff --git a/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6 b/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6 index 5682cd2fbd..e62cf98405 100644 --- a/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6 +++ b/app/assets/javascripts/discourse/helpers/bound-avatar-template.js.es6 @@ -1,4 +1,10 @@ import { htmlHelper } from 'discourse-common/lib/helpers'; import { avatarImg } from 'discourse/lib/utilities'; -export default htmlHelper((avatarTemplate, size) => avatarImg({ size, avatarTemplate })); +export default htmlHelper((avatarTemplate, size) => { + if (Ember.isEmpty(avatarTemplate)) { + return "
"; + } else { + return avatarImg({ size, avatarTemplate }); + } +}); diff --git a/app/assets/javascripts/discourse/helpers/format-age.js.es6 b/app/assets/javascripts/discourse/helpers/format-age.js.es6 index 75119d0c5f..a2a52d3d8c 100644 --- a/app/assets/javascripts/discourse/helpers/format-age.js.es6 +++ b/app/assets/javascripts/discourse/helpers/format-age.js.es6 @@ -1,7 +1,11 @@ -import { autoUpdatingRelativeAge } from 'discourse/lib/formatter'; +import { autoUpdatingRelativeAge, durationTiny } from 'discourse/lib/formatter'; import { registerUnbound } from 'discourse-common/lib/helpers'; registerUnbound('format-age', function(dt) { dt = new Date(dt); return new Handlebars.SafeString(autoUpdatingRelativeAge(dt)); }); + +registerUnbound('format-duration', function(seconds) { + return new Handlebars.SafeString(durationTiny(seconds)); +}); diff --git a/app/assets/javascripts/discourse/helpers/format-username.js.es6 b/app/assets/javascripts/discourse/helpers/format-username.js.es6 new file mode 100644 index 0000000000..dcb8be1840 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/format-username.js.es6 @@ -0,0 +1,4 @@ +import { registerUnbound } from 'discourse-common/lib/helpers'; +import { formatUsername } from 'discourse/lib/utilities'; + +export default registerUnbound('format-username', formatUsername); diff --git a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 index 75e60da098..71e6e87249 100644 --- a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 +++ b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 @@ -1,5 +1,5 @@ import { registerUnbound } from 'discourse-common/lib/helpers'; -import { avatarImg } from 'discourse/lib/utilities'; +import { avatarImg, formatUsername } from 'discourse/lib/utilities'; function renderAvatar(user, options) { options = options || {}; @@ -11,6 +11,8 @@ function renderAvatar(user, options) { if (!username || !avatarTemplate) { return ''; } + let formattedUsername = formatUsername(username); + let title = options.title; if (!title && !options.ignoreTitle) { // first try to get a title @@ -22,7 +24,7 @@ function renderAvatar(user, options) { // if a description has been provided if (description && description.length > 0) { // preprend the username before the description - title = username + " - " + description; + title = formattedUsername + " - " + description; } } } @@ -30,7 +32,7 @@ function renderAvatar(user, options) { return avatarImg({ size: options.imageSize, extraClasses: Em.get(user, 'extras') || options.extraClasses, - title: title || username, + title: title || formattedUsername, avatarTemplate: avatarTemplate }); } else { diff --git a/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6 deleted file mode 100644 index 3d8c2f7fd2..0000000000 --- a/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6 +++ /dev/null @@ -1,16 +0,0 @@ -// Android Chrome App Banner requires at least **one** service worker to be instantiate and https. -// After Discourse starts to use service workers for other stuff (like mobile notification, offline mode, or ember) -// we can ditch this. - -export default { - name: 'android-app-banner-service-worker', - - initialize(container) { - const caps = container.lookup('capabilities:main'); - const isSecure = document.location.protocol === 'https:'; - - if (isSecure && caps.isAndroid && 'serviceWorker' in navigator) { - navigator.serviceWorker.register(Discourse.BaseUri + '/service-worker.js', {scope: './'}); - } - } -}; diff --git a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 new file mode 100644 index 0000000000..7c16106dda --- /dev/null +++ b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 @@ -0,0 +1,12 @@ +export default { + name: 'register-service-worker', + + initialize() { + const isSecure = (document.location.protocol === 'https:') || + (location.hostname === "localhost"); + + if (isSecure && ('serviceWorker' in navigator)) { + navigator.serviceWorker.register(`${Discourse.BaseUri}/service-worker.js`); + } + } +}; diff --git a/app/assets/javascripts/discourse/lib/formatter.js.es6 b/app/assets/javascripts/discourse/lib/formatter.js.es6 index b478ddfa43..b26b270b2c 100644 --- a/app/assets/javascripts/discourse/lib/formatter.js.es6 +++ b/app/assets/javascripts/discourse/lib/formatter.js.es6 @@ -129,6 +129,58 @@ function wrapAgo(dateStr) { return I18n.t("dates.wrap_ago", { date: dateStr }); } +export function durationTiny(distance, ageOpts) { + if (typeof(distance) !== 'number') { return '—'; } + + const dividedDistance = Math.round(distance / 60.0); + const distanceInMinutes = (dividedDistance < 1) ? 1 : dividedDistance; + + const t = function(key, opts) { + const result = I18n.t("dates.tiny." + key, opts); + return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result; + }; + + let formatted; + + switch(true) { + case(distance <= 59): + formatted = t("less_than_x_minutes", {count: 1}); + break; + case(distanceInMinutes >= 0 && distanceInMinutes <= 44): + formatted = t("x_minutes", {count: distanceInMinutes}); + break; + case(distanceInMinutes >= 45 && distanceInMinutes <= 89): + formatted = t("about_x_hours", {count: 1}); + break; + case(distanceInMinutes >= 90 && distanceInMinutes <= 1409): + formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)}); + break; + case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519): + formatted = t("x_days", {count: 1}); + break; + case(distanceInMinutes >= 2520 && distanceInMinutes <= 129599): + formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)}); + break; + case(distanceInMinutes >= 129600 && distanceInMinutes <= 525599): + formatted = t("x_months", {count: Math.round(distanceInMinutes / 43200.0)}); + break; + default: + const numYears = distanceInMinutes / 525600.0; + const remainder = numYears % 1; + if (remainder < 0.25) { + formatted = t("about_x_years", {count: parseInt(numYears)}); + } else if (remainder < 0.75) { + formatted = t("over_x_years", {count: parseInt(numYears)}); + } else { + formatted = t("almost_x_years", {count: parseInt(numYears) + 1}); + } + + break; + } + + return formatted; +} + function relativeAgeTiny(date, ageOpts) { const format = "tiny"; const distance = Math.round((new Date() - date) / 1000); diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 index d8cd970299..688207c5b9 100644 --- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 @@ -1,5 +1,6 @@ import { ajax } from 'discourse/lib/ajax'; import { userPath } from 'discourse/lib/url'; +import { formatUsername } from 'discourse/lib/utilities'; function replaceSpan($e, username, opts) { let extra = ""; @@ -16,7 +17,7 @@ function replaceSpan($e, username, opts) { extra = `data-name='${username}'`; extraClass = "cannot-see"; } - $e.replaceWith(`
@${username}`); + $e.replaceWith(`@${formatUsername(username)}`); } } diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 38772203ae..68cc7eb8e6 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -20,10 +20,11 @@ import { addPostTransformCallback } from 'discourse/widgets/post-stream'; import { attachAdditionalPanel } from 'discourse/widgets/header'; import { registerIconRenderer, replaceIcon } from 'discourse-common/lib/icon-library'; import { addNavItem } from 'discourse/models/nav-item'; - +import { replaceFormatter } from 'discourse/lib/utilities'; +import { modifySelectKit } from "select-kit/mixins/plugin-api"; // If you add any methods to the API ensure you bump up this number -const PLUGIN_API_VERSION = '0.8.11'; +const PLUGIN_API_VERSION = '0.8.13'; class PluginApi { constructor(version, container) { @@ -570,6 +571,40 @@ class PluginApi { addNavItem(item); } } + + + /** + * + * Registers a function that will format a username when displayed. This will not + * be applied when the username is used as an `id` or in URL strings. + * + * Example: + * + * ``` + * // display usernames in UPPER CASE + * api.formatUsername(username => username.toUpperCase()); + * + * ``` + * + **/ + formatUsername(fn) { + replaceFormatter(fn); + } + + /** + * + * Access SelectKit plugin api + * + * Example: + * + * modifySelectKit("topic-footer-mobile-dropdown").appendContent(() => [{ + * name: "discourse", + * id: 1 + * }]) + */ + modifySelectKit(pluginApiKey) { + return modifySelectKit(pluginApiKey); + } } let _pluginv01; diff --git a/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6 b/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6 index 3b747284fd..86b30b60bf 100644 --- a/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6 +++ b/app/assets/javascripts/discourse/lib/render-topic-featured-link.js.es6 @@ -1,4 +1,3 @@ -import { extractDomainFromUrl } from 'discourse/lib/utilities'; import { h } from 'virtual-dom'; const _decorators = []; @@ -7,24 +6,23 @@ export function addFeaturedLinkMetaDecorator(decorator) { _decorators.push(decorator); } -function extractLinkMeta(topic) { - const href = topic.featured_link, - target = Discourse.User.currentProp('external_links_in_new_tab') ? '_blank' : ''; +export function extractLinkMeta(topic) { + const href = topic.get('featured_link'); + const target = Discourse.User.currentProp('external_links_in_new_tab') ? '_blank' : ''; if (!href) { return; } - let domain = extractDomainFromUrl(href); - if (!domain) { return; } + const meta = { + target: target, + href, + domain: topic.get('featured_link_root_domain'), + rel: 'nofollow' + }; - // www appears frequently, so we truncate it - if (domain && domain.substr(0, 4) === 'www.') { - domain = domain.substring(4); - } - - const meta = { target, href, domain, rel: 'nofollow' }; if (_decorators.length) { _decorators.forEach(cb => cb(meta)); } + return meta; } @@ -45,4 +43,3 @@ export function topicFeaturedLinkNode(topic) { }, meta.domain); } } - diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 index 42777ed4f1..7cb249b15e 100644 --- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 +++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 @@ -1,10 +1,4 @@ -export function isAppleDevice() { - // IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone - // This will apply hack on all iDevices - return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) && - navigator.userAgent.match(/Safari/g) && - !navigator.userAgent.match(/Trident/g); -} +import { isAppleDevice } from 'discourse/lib/utilities'; // we can't tell what the actual visible window height is // because we cannot account for the height of the mobile keyboard diff --git a/app/assets/javascripts/discourse/lib/show-modal.js.es6 b/app/assets/javascripts/discourse/lib/show-modal.js.es6 index 10e7695b57..179a700da5 100644 --- a/app/assets/javascripts/discourse/lib/show-modal.js.es6 +++ b/app/assets/javascripts/discourse/lib/show-modal.js.es6 @@ -1,7 +1,37 @@ +import { isAppleDevice } from 'discourse/lib/utilities'; + export default function(name, opts) { opts = opts || {}; const container = Discourse.__container__; + + // iOS 11 -> 11.1 have broken INPUTs on position fixed + // if for any reason there is a body higher than 100% behind them. + // What happens is that when INPUTs gets focus they shift the body + // which ends up moving the cursor to an invisible spot + // this makes the login experience on iOS painful, user thinks it is broken. + // + // Also, very little value in showing main outlet and header on iOS + // anyway, so just hide it. + if (isAppleDevice()) { + let pos = $(window).scrollTop(); + $(window) + .off('show.bs.modal.ios-hacks') + .on('show.bs.modal.ios-hacks', () => { + $('#main-outlet, header').hide(); + }); + + $(window) + .off('hide.bs.modal.ios-hacks') + .on('hide.bs.modal.ios-hacks', () => { + $('#main-outlet, header').show(); + $(window).scrollTop(pos); + + $(window).off('hide.bs.modal.ios-hacks'); + $(window).off('show.bs.modal.ios-hacks'); + }); + } + // We use the container here because modals are like singletons // in Discourse. Only one can be shown with a particular state. const route = container.lookup('route:application'); diff --git a/app/assets/javascripts/discourse/lib/text.js.es6 b/app/assets/javascripts/discourse/lib/text.js.es6 index 874f519010..09a7592828 100644 --- a/app/assets/javascripts/discourse/lib/text.js.es6 +++ b/app/assets/javascripts/discourse/lib/text.js.es6 @@ -3,6 +3,7 @@ import { performEmojiUnescape, buildEmojiUrl } from 'pretty-text/emoji'; import WhiteLister from 'pretty-text/white-lister'; import { sanitize as textSanitize } from 'pretty-text/sanitizer'; import loadScript from 'discourse/lib/load-script'; +import { formatUsername } from 'discourse/lib/utilities'; function getOpts(opts) { const siteSettings = Discourse.__container__.lookup('site-settings:main'), @@ -12,7 +13,8 @@ function getOpts(opts) { getURL: Discourse.getURLWithCDN, currentUser: Discourse.__container__.lookup('current-user:main'), censoredWords: site.censored_words, - siteSettings + siteSettings, + formatUsername }, opts); return buildOptions(opts); diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 1f989f297e..2fe1b79a8b 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -193,10 +193,10 @@ const DiscourseURL = Ember.Object.extend({ path = path.replace(/(https?\:)?\/\/[^\/]+/, ''); // Rewrite /my/* urls - if (path.indexOf('/my/') === 0) { + if (path.indexOf(Discourse.BaseUri + '/my/') === 0) { const currentUser = Discourse.User.current(); if (currentUser) { - path = path.replace('/my/', userPath(currentUser.get('username_lower') + "/")); + path = path.replace(Discourse.BaseUri + '/my/', userPath(currentUser.get('username_lower') + "/")); } else { document.location.href = "/404"; return; diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index a1e55234e2..1b4cfda349 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -1,5 +1,7 @@ import { escape } from 'pretty-text/sanitizer'; +const homepageSelector = 'meta[name=discourse_current_homepage]'; + export function translateSize(size) { switch (size) { case 'tiny': return 20; @@ -21,6 +23,17 @@ export function escapeExpression(string) { return escape(string); } +let _usernameFormatDelegate = username => username; + +export function formatUsername(username) { + return _usernameFormatDelegate(username || ''); +} + +export function replaceFormatter(fn) { + _usernameFormatDelegate = fn; +} + + export function avatarUrl(template, size) { if (!template) { return ""; } const rawSize = getRawSize(translateSize(size)); @@ -272,6 +285,21 @@ function uploadTypeFromFileName(fileName) { return isAnImage(fileName) ? 'image' : 'attachment'; } +function isGUID(value) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); +} + +function imageNameFromFileName(fileName) { + const split = fileName.split('.'); + const name = split[split.length-2]; + + if (exports.isAppleDevice() && isGUID(name)) { + return I18n.t('upload_selector.default_image_alt_text'); + } + + return name; +} + export function allowsImages() { return authorizesAllExtensions() || IMAGES_EXTENSIONS_REGEX.test(authorizedExtensions()); } @@ -296,8 +324,7 @@ export function uploadLocation(url) { export function getUploadMarkdown(upload) { if (isAnImage(upload.original_filename)) { - const split = upload.original_filename.split('.'); - const name = split[split.length-2]; + const name = imageNameFromFileName(upload.original_filename); return `![${name}|${upload.width}x${upload.height}](${upload.short_url || upload.url})`; } else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i).test(upload.original_filename)) { return uploadLocation(upload.url); @@ -338,8 +365,22 @@ export function displayErrorForUpload(data) { } export function defaultHomepage() { - // the homepage is the first item of the 'top_menu' site setting - return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; + let homepage = null; + let elem = _.first($(homepageSelector)); + if (elem) { + homepage = elem.content; + } + if (!homepage) { + homepage = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; + } + return homepage; +} + +export function setDefaultHomepage(homepage) { + let elem = _.first($(homepageSelector)); + if (elem) { + elem.content = homepage; + } } export function determinePostReplaceSelection({ selection, needle, replacement }) { @@ -372,5 +413,13 @@ export function determinePostReplaceSelection({ selection, needle, replacement } } } +export function isAppleDevice() { + // IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone + // This will apply hack on all iDevices + return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) && + navigator.userAgent.match(/Safari/g) && + !navigator.userAgent.match(/Trident/g); +} + // This prevents a mini racer crash export default {}; diff --git a/app/assets/javascripts/discourse/mixins/password-validation.js.es6 b/app/assets/javascripts/discourse/mixins/password-validation.js.es6 index 7e9caf62dc..b65272e03f 100644 --- a/app/assets/javascripts/discourse/mixins/password-validation.js.es6 +++ b/app/assets/javascripts/discourse/mixins/password-validation.js.es6 @@ -16,13 +16,13 @@ export default Ember.Mixin.create({ return I18n.t('user.password.instructions', {count: this.get('passwordMinLength')}); }, - @computed('isDeveloper') - passwordMinLength() { - return this.get('isDeveloper') ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length; + @computed('isDeveloper', 'admin') + passwordMinLength(isDeveloper, admin) { + return (isDeveloper || admin) ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length; }, - @computed('accountPassword', 'passwordRequired', 'rejectedPasswords.[]', 'accountUsername', 'accountEmail', 'isDeveloper') - passwordValidation(password, passwordRequired, rejectedPasswords, accountUsername, accountEmail, isDeveloper) { + @computed('accountPassword', 'passwordRequired', 'rejectedPasswords.[]', 'accountUsername', 'accountEmail', 'passwordMinLength') + passwordValidation(password, passwordRequired, rejectedPasswords, accountUsername, accountEmail, passwordMinLength) { if (!passwordRequired) { return InputValidation.create({ ok: true }); } @@ -40,8 +40,7 @@ export default Ember.Mixin.create({ } // If too short - const passwordLength = isDeveloper ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length; - if (password.length < passwordLength) { + if (password.length < passwordMinLength) { return InputValidation.create({ failed: true, reason: I18n.t('user.password.too_short') diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6 index ec76aa1e66..1dc55eeb85 100644 --- a/app/assets/javascripts/discourse/mixins/upload.js.es6 +++ b/app/assets/javascripts/discourse/mixins/upload.js.es6 @@ -18,12 +18,9 @@ export default Em.Mixin.create({ uploadUrl = Discourse.getURL(this.getWithDefault("uploadUrl", "/uploads")), reset = () => this.setProperties({ uploading: false, uploadProgress: 0}); - this.messageBus.subscribe("/uploads/" + this.get("type"), upload => { - if (upload && upload.url) { - this.uploadDone(upload); - } else { - displayErrorForUpload(upload); - } + $upload.on("fileuploaddone", (e, data) => { + let upload = data.result; + this.uploadDone(upload); reset(); }); @@ -59,7 +56,7 @@ export default Em.Mixin.create({ }); $upload.on("fileuploadfail", (e, data) => { - displayErrorForUpload(data); + displayErrorForUpload(data.jqXHR.responseJSON); reset(); }); }.on("didInsertElement"), diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6 index 16304f453e..52c3eb443c 100644 --- a/app/assets/javascripts/discourse/models/category.js.es6 +++ b/app/assets/javascripts/discourse/models/category.js.es6 @@ -90,6 +90,7 @@ const Category = RestModel.extend({ position: this.get('position'), email_in: this.get('email_in'), email_in_allow_strangers: this.get('email_in_allow_strangers'), + mailinglist_mirror: this.get('mailinglist_mirror'), parent_category_id: this.get('parent_category_id'), uploaded_logo_id: this.get('uploaded_logo.id'), uploaded_background_id: this.get('uploaded_background.id'), @@ -205,9 +206,7 @@ Category.reopenClass({ }, list() { - return Discourse.SiteSettings.fixed_category_positions ? - Discourse.Site.currentProp('categories') : - Discourse.Site.currentProp('sortedCategories'); + return Discourse.Site.currentProp('categoriesList'); }, listByActivity() { diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 48e8e26196..37ac8b9eba 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -46,6 +46,12 @@ const CLOSED = 'closed', featuredLink: 'topic.featured_link' }; +const _saveLabels = {}; +_saveLabels[EDIT] = 'composer.save_edit'; +_saveLabels[REPLY] = 'composer.reply'; +_saveLabels[CREATE_TOPIC] = 'composer.create_topic'; +_saveLabels[PRIVATE_MESSAGE] = 'composer.create_pm'; + const Composer = RestModel.extend({ _categoryId: null, unlistTopic: false, @@ -85,7 +91,7 @@ const Composer = RestModel.extend({ @computed("privateMessage", "archetype.hasOptions") showCategoryChooser(isPrivateMessage, hasOptions) { - const manyCategories = Discourse.Category.list().length > 1; + const manyCategories = this.site.get('categories').length > 1; return !isPrivateMessage && (hasOptions || manyCategories); }, @@ -250,14 +256,9 @@ const Composer = RestModel.extend({ } }, - @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'; - } + @computed('action', 'whisper') + saveLabel(action, whisper) { + return whisper ? 'composer.create_whisper' : _saveLabels[action]; }, hasMetaData: function() { @@ -274,6 +275,16 @@ const Composer = RestModel.extend({ return this.get('reply') !== this.get('originalText'); }.property('reply', 'originalText'), + /** + Did the user make changes to the topic title? + + @property titleDirty + **/ + @computed('title', 'originalTitle') + titleDirty(title, originalTitle) { + return title !== originalTitle; + }, + /** Number of missing characters in the title until valid. @@ -481,7 +492,7 @@ const Composer = RestModel.extend({ this.set('categoryId', opts.categoryId || this.get('topic.category.id')); if (!this.get('categoryId') && this.get('creatingTopic')) { - const categories = Discourse.Category.list(); + const categories = this.site.get('categories'); if (categories.length === 1) { this.set('categoryId', categories[0].get('id')); } @@ -518,6 +529,9 @@ const Composer = RestModel.extend({ } if (opts.title) { this.set('title', opts.title); } this.set('originalText', opts.draft ? '' : this.get('reply')); + if (this.get('editingFirstPost')) { + this.set('originalTitle', this.get('title')); + } return false; }, @@ -740,10 +754,18 @@ const Composer = RestModel.extend({ saveDraft() { // Do not save when drafts are disabled if (this.get('disableDrafts')) return; - // Do not save when there is no reply - if (!this.get('reply')) return; - // Do not save when the reply's length is too small - if (this.get('replyLength') < this.siteSettings.min_post_length) return; + + if (this.get('canEditTitle')) { + // Save title and/or post body + if (!this.get('title') && !this.get('reply')) return; + if (this.get('title') && this.get('titleLengthValid') && + this.get('reply') && this.get('replyLength') < this.siteSettings.min_post_length) return; + } else { + // Do not save when there is no reply + if (!this.get('reply')) return; + // Do not save when the reply's length is too small + if (this.get('replyLength') < this.siteSettings.min_post_length) return; + } const data = { reply: this.get('reply'), diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index c7e5eed6b1..c26544e7c5 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -203,12 +203,13 @@ const Group = RestModel.extend({ findPosts(opts) { opts = opts || {}; - const type = opts['type'] || 'posts'; + const type = opts.type || 'posts'; var data = {}; if (opts.beforePostId) { data.before_post_id = opts.beforePostId; } + if (opts.categoryId) { data.category_id = parseInt(opts.categoryId); } - return ajax(`/groups/${this.get('name')}/${type}.json`, { data: data }).then(posts => { + return ajax(`/groups/${this.get('name')}/${type}.json`, { data }).then(posts => { return posts.map(p => { p.user = User.create(p.user); p.topic = Topic.create(p.topic); diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index f71842cd6e..7616644968 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -111,6 +111,7 @@ NavItem.reopenClass({ opts = opts || {}; if (anonymous && !Discourse.Site.currentProp('anonymous_top_menu_items').includes(testName)) return null; + if (!Discourse.Category.list() && testName === "categories") return null; if (!Discourse.Site.currentProp('top_menu_items').includes(testName)) return null; diff --git a/app/assets/javascripts/discourse/models/site.js.es6 b/app/assets/javascripts/discourse/models/site.js.es6 index ace9f7176c..073f6b43f9 100644 --- a/app/assets/javascripts/discourse/models/site.js.es6 +++ b/app/assets/javascripts/discourse/models/site.js.es6 @@ -54,6 +54,14 @@ const Site = RestModel.extend({ return result; }, + // Returns it in the correct order, by setting + @computed + categoriesList() { + return this.siteSettings.fixed_category_positions ? + this.get('categories') : + this.get('sortedCategories'); + }, + postActionTypeById(id) { return this.get("postActionByIdLookup.action" + id); }, diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 3727290485..df6fd563cc 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -135,7 +135,7 @@ const Topic = RestModel.extend({ const categoryName = this.get('categoryName'); let category; if (categoryName) { - category = Discourse.Category.list().findBy('name', categoryName); + category = this.site.get('categories').findBy('name', categoryName); } this.set('category', category); }.observes('categoryName'), diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 17e7f9bfc1..9925d4798a 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -16,6 +16,8 @@ import PreloadStore from 'preload-store'; import { defaultHomepage } from 'discourse/lib/utilities'; import { userPath } from 'discourse/lib/url'; +const isForever = dt => moment().diff(dt, 'years') < -500; + const User = RestModel.extend({ hasPMs: Em.computed.gt("private_messages_stats.all", 0), @@ -178,14 +180,16 @@ const User = RestModel.extend({ }, @computed("suspended_till") - suspendedForever(suspendedTill) { - return moment().diff(suspendedTill, 'years') < -500; - }, + suspendedForever: isForever, + + @computed("silenced_till") + silencedForever: isForever, @computed("suspended_till") - suspendedTillDate(suspendedTill) { - return longDate(suspendedTill); - }, + suspendedTillDate: longDate, + + @computed("silenced_till") + silencedTillDate: longDate, changeUsername(new_username) { return ajax(userPath(`${this.get('username_lower')}/preferences/username`), { @@ -249,6 +253,7 @@ const User = RestModel.extend({ 'include_tl0_in_digests', 'theme_key', 'allow_private_messages', + 'homepage_id', ]; if (fields) { @@ -461,7 +466,7 @@ const User = RestModel.extend({ "delete": function() { if (this.get('can_delete_account')) { - return ajax(userPath(this.get('username')), { + return ajax(userPath(this.get('username') + ".json"), { type: 'DELETE', data: {context: window.location.pathname} }); @@ -472,7 +477,7 @@ const User = RestModel.extend({ dismissBanner(bannerKey) { this.set("dismissed_banner_key", bannerKey); - ajax(userPath(this.get('username')), { + ajax(userPath(this.get('username') + ".json"), { type: 'PUT', data: { dismissed_banner_key: bannerKey } }); diff --git a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 index ca60c3c311..453ee0612b 100644 --- a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 @@ -36,14 +36,14 @@ export default { app.register('topic-tracking-state:main', topicTrackingState, { instantiate: false }); ALL_TARGETS.forEach(t => app.inject(t, 'topicTrackingState', 'topic-tracking-state:main')); - const site = Discourse.Site.current(); - app.register('site:main', site, { instantiate: false }); - ALL_TARGETS.forEach(t => app.inject(t, 'site', 'site:main')); - const siteSettings = Discourse.SiteSettings; app.register('site-settings:main', siteSettings, { instantiate: false }); ALL_TARGETS.forEach(t => app.inject(t, 'siteSettings', 'site-settings:main')); + const site = Discourse.Site.current(); + app.register('site:main', site, { instantiate: false }); + ALL_TARGETS.forEach(t => app.inject(t, 'site', 'site:main')); + app.register('search-service:main', SearchService); ALL_TARGETS.forEach(t => app.inject(t, 'searchService', 'search-service:main')); diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index 0f8fb98d39..4b29783e56 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -49,7 +49,6 @@ export default (filterArg, params) => { category, filterMode: filterMode, noSubcategories: params && params.no_subcategories, - canEditCategory: category.get('can_edit') }); }, diff --git a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 index d254e0e6a2..0ba2194e50 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 @@ -12,10 +12,6 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { this.render("discovery/categories", { outlet: "list-container" }); }, - beforeModel() { - this.controllerFor("navigation/categories").set("filterMode", "categories"); - }, - model() { const style = !this.site.mobileView && this.siteSettings.desktop_category_page_style; const parentCategory = this.get("model.parentCategory"); @@ -81,7 +77,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { controller.set("model", model); this.controllerFor("navigation/categories").setProperties({ - canCreateCategory: model.get("can_create_category"), + showCategoryAdmin: model.get("can_create_category"), canCreateTopic: model.get("can_create_topic"), }); diff --git a/app/assets/javascripts/discourse/routes/group-activity.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-index.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/routes/group-activity.js.es6 rename to app/assets/javascripts/discourse/routes/group-activity-index.js.es6 diff --git a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 index 03cd7ec114..827c8c468b 100644 --- a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 @@ -6,12 +6,13 @@ export function buildGroupPage(type) { return I18n.t(`groups.${type}`); }, - model() { - return this.modelFor("group").findPosts({ type }); + model(params, transition) { + let categoryId = Ember.get(transition, 'queryParams.category_id'); + return this.modelFor("group").findPosts({ type, categoryId }); }, setupController(controller, model) { - this.controllerFor('group-activity-posts').setProperties({ model, type }); + this.controllerFor('group-activity-posts').setProperties({ model, type, canLoadMore: true }); this.controllerFor("group").set("showing", type); }, diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 index c4f3a3d913..19d84f9b7e 100644 --- a/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 @@ -8,6 +8,14 @@ export default createPMRoute('groups', 'private-messages-groups').extend({ }); }, + afterModel(model) { + const split = model.get("filter").split('/'); + const groupName = split[split.length-2]; + const groups = this.modelFor("user").get("groups"); + const group = _.first(groups.filterBy("name", groupName)); + this.controllerFor("user-private-messages").set("group", group); + }, + setupController(controller, model) { this._super.apply(this, arguments); const split = model.get("filter").split('/'); diff --git a/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs b/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs deleted file mode 100644 index 4856de5179..0000000000 --- a/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/assets/javascripts/discourse/templates/components/category-selector.hbs b/app/assets/javascripts/discourse/templates/components/category-selector.hbs deleted file mode 100644 index a265ef5f12..0000000000 --- a/app/assets/javascripts/discourse/templates/components/category-selector.hbs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs index 3d3f6fc8eb..3c7f581375 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs @@ -11,30 +11,9 @@ validation=validation loading=composer.loading forcePreview=forcePreview - composerEvents=true}} + composerEvents=true + autofocus=true}} -
- {{#if site.mobileView}} +{{#if site.mobileView}} - {{i18n 'upload'}} - - {{#if showPreview}} - {{d-button action='togglePreview' class='hide-preview' label='composer.hide_preview'}} - {{/if}} - {{else}} - {{{toggleText}}} - {{/if}} - - {{#if isUploading}} -
- {{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} - {{uploadProgress}}% - {{#if isCancellable}} - {{d-icon "times"}} - {{/if}} -
- {{/if}} -
- {{draftStatus}} -
-
+{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs b/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs index 09e841bd85..93263ae3cb 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs @@ -1,12 +1,14 @@ -{{#if site.mobileView}} - {{flat-button - class="toggle-toolbar" - icon="bars" - action=toggleToolbar}} -{{/if}} +
+ {{#if site.mobileView}} + {{flat-button + class="toggle-toolbar" + icon="bars" + action=toggleToolbar}} + {{/if}} -{{flat-button - class="toggler" - icon=toggleIcon - action=toggleComposer - title='composer.toggler'}} + {{flat-button + class="toggler" + icon=toggleIcon + action=toggleComposer + title=title}} +
diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index 961b63c6ef..c928412ebc 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -1,4 +1,5 @@ +
{{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction="insertLink"}}

{{i18n "composer.link_dialog_title"}}

@@ -8,19 +9,18 @@
-
- {{#each toolbar.groups as |group|}} - {{#each group.buttons as |b|}} - {{d-button action=b.action actionParam=b translatedTitle=b.title label=b.label icon=b.icon class=b.className}} - {{/each}} - {{#unless group.lastGroup}} -
- {{/unless}} - {{/each}} -
-
-
+
+ {{#each toolbar.groups as |group|}} + {{#each group.buttons as |b|}} + {{d-button action=b.action actionParam=b translatedTitle=b.title label=b.label icon=b.icon class=b.className}} + {{/each}} + {{#unless group.lastGroup}} +
+ {{/unless}} + {{/each}} +
+ {{conditional-loading-spinner condition=loading}} {{textarea tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated}} {{popup-input-tip validation=validation}} @@ -30,7 +30,7 @@
{{{preview}}}
- {{plugin-outlet name="editor-preview"}} + {{plugin-outlet name="editor-preview" classNames="d-editor-plugin"}}
diff --git a/app/assets/javascripts/discourse/templates/components/d-navigation.hbs b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs new file mode 100644 index 0000000000..6f8c71f524 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs @@ -0,0 +1,26 @@ +{{bread-crumbs categories=categories category=category noSubcategories=noSubcategories}} + +{{#if showCategoryAdmin}} + {{categories-admin-dropdown + create=createCategory + reorder=reorderCategories}} +{{/if}} + +{{navigation-bar navItems=navItems filterMode=filterMode category=category}} + +{{#if showCategoryNotifications}} + {{category-notifications-button value=category.notification_level category=category}} +{{/if}} + +{{create-topic-button + canCreateTopic=canCreateTopic + action=createTopic + disabled=createTopicDisabled}} + +{{#if showCategoryEdit}} + {{d-button + class="btn-default edit-category" + action=editCategory + icon="wrench" + label="category.edit_long"}} +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/directory-item.hbs b/app/assets/javascripts/discourse/templates/components/directory-item.hbs index 4b1d9a940e..e7758227f4 100644 --- a/app/assets/javascripts/discourse/templates/components/directory-item.hbs +++ b/app/assets/javascripts/discourse/templates/components/directory-item.hbs @@ -7,5 +7,5 @@ {{number item.posts_read}} {{number item.days_visited}} {{#if showTimeRead}} - {{unbound item.time_read}} + {{format-duration item.time_read}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs index c988d53869..c31406819a 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs @@ -22,7 +22,9 @@ {{category-chooser none="category.none" value=category.parent_category_id + excludeCategoryId=category.id categories=parentCategories + allowSubCategories=false allowUncategorized=false}} {{/if}} @@ -37,28 +39,30 @@ {{i18n 'category.no_description'}} {{/if}} {{#if category.topic_url}} -
+
{{d-button class="btn-small" action="showCategoryTopic" icon="pencil" label="category.change_in_category_topic"}} {{/if}} {{/if}} -
- -
- {{{categoryBadgePreview}}} + {{#unless noCategoryStyle}} +
+ +
+ {{{categoryBadgePreview}}} -
- {{i18n 'category.background_color'}}: - #{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}} - {{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}} -
+
+ {{i18n 'category.background_color'}}: + #{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}} + {{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}} +
-
- {{i18n 'category.foreground_color'}}: - #{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}} - {{color-picker colors=foregroundColors value=category.text_color id='edit-text-color'}} +
+ {{i18n 'category.foreground_color'}}: + #{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}} + {{color-picker colors=foregroundColors value=category.text_color id='edit-text-color'}} +
-
-
+
+ {{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs index a389e2ceab..fb0a13c213 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs @@ -9,21 +9,30 @@ {{{i18n "category.can"}}} {{p.permission.description}} {{#if editingPermissions}} - {{d-icon "times-circle"}} + {{d-icon "times-circle"}} {{/if}} {{/each}} {{#if editingPermissions}} - {{combo-box content=category.availableGroups value=selectedGroup}} - {{combo-box class="permission-selector" - nameProperty="description" - content=category.availablePermissions - value=selectedPermission}} - + {{#if category.availableGroups}} + {{combo-box class="available-groups" + allowInitialValueMutation=true + allowsContentReplacement=true + content=category.availableGroups + value=selectedGroup}} + {{combo-box allowInitialValueMutation=true + class="permission-selector" + nameProperty="description" + content=category.availablePermissions + value=selectedPermission}} + + {{/if}} {{else}} {{#unless category.is_special}} - + {{/unless}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs index f209d17af2..fa7b2a5653 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs @@ -47,15 +47,19 @@
+
+ {{combo-box valueAttribute="value" content=availableViews value=category.default_view}} +
+
+ {{combo-box valueAttribute="value" content=availableTopPeriods value=category.default_top_period}} +
@@ -115,6 +119,13 @@
+
+ +
+ {{plugin-outlet name="category-email-in" args=(hash category=category)}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs index c7929241b8..f90534931b 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs @@ -1,6 +1,6 @@
- {{combo-box content=timerTypes value=selection width="50%"}} + {{combo-box class="timer-type" allowInitialValueMutation=true content=timerTypes value=selection}}
diff --git a/app/assets/javascripts/discourse/templates/components/expand-post.hbs b/app/assets/javascripts/discourse/templates/components/expand-post.hbs index 94ba1d6290..63716b696c 100644 --- a/app/assets/javascripts/discourse/templates/components/expand-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/expand-post.hbs @@ -1,5 +1,11 @@ {{#if item.truncated}} - - {{d-icon "chevron-down"}} - + {{#if expanded}} + + {{d-icon "chevron-up"}} + + {{else}} + + {{d-icon "chevron-down"}} + + {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/group-activity-filter.hbs b/app/assets/javascripts/discourse/templates/components/group-activity-filter.hbs new file mode 100644 index 0000000000..68623d09c8 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/group-activity-filter.hbs @@ -0,0 +1,3 @@ +{{#link-to (concat 'group.activity.' filter) (query-params category_id=categoryId)}} + {{i18n (concat 'groups.' filter)}} +{{/link-to}} diff --git a/app/assets/javascripts/discourse/templates/components/group-navigation.hbs b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs new file mode 100644 index 0000000000..8501e9e2f5 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs @@ -0,0 +1,11 @@ +{{#mobile-nav class='group-nav' desktopClass="nav nav-pills" currentPath=currentPath}} + {{#each availableTabs as |tab|}} +
  • + {{#link-to tab.route group title=tab.message class=tab.name}} + {{#if tab.icon}}{{d-icon tab.icon}}{{/if}} + {{tab.message}} + {{#if tab.count}}({{tab.count}}){{/if}} + {{/link-to}} +
  • + {{/each}} +{{/mobile-nav}} diff --git a/app/assets/javascripts/discourse/templates/components/group-post.hbs b/app/assets/javascripts/discourse/templates/components/group-post.hbs index 66386d75ae..9f51a1584f 100644 --- a/app/assets/javascripts/discourse/templates/components/group-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-post.hbs @@ -1,19 +1,32 @@ -
    {{!-- DEPRECATED: 'item' class --}} -
    -
    {{avatar post.user imageSize="large" extraClasses="actor" ignoreTitle="true"}}
    - {{format-date post.created_at leaveAgo="true"}} - {{expand-post item=post}} - - {{{post.topic.fancyTitle}}} - - {{category-link post.category}} -
    +
    +
    + + {{avatar post.user imageSize="large" extraClasses="actor" ignoreTitle="true"}} + +
    + -

    - {{{post.excerpt}}} -

    +
    +
    + {{#if post.expandedExcerpt}} + {{{post.expandedExcerpt}}} + {{else}} + {{{post.excerpt}}} + {{/if}}
    diff --git a/app/assets/javascripts/discourse/templates/components/list-setting.hbs b/app/assets/javascripts/discourse/templates/components/list-setting.hbs deleted file mode 100644 index 830982fc47..0000000000 --- a/app/assets/javascripts/discourse/templates/components/list-setting.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
    - -
    diff --git a/app/assets/javascripts/discourse/templates/components/navigation-bar.hbs b/app/assets/javascripts/discourse/templates/components/navigation-bar.hbs index 256ab2af04..0808efcc6d 100644 --- a/app/assets/javascripts/discourse/templates/components/navigation-bar.hbs +++ b/app/assets/javascripts/discourse/templates/components/navigation-bar.hbs @@ -1,5 +1,5 @@ {{#each navItems as |navItem|}} - {{navigation-item content=navItem filterMode=filterMode}} + {{navigation-item content=navItem filterMode=filterMode category=category}} {{/each}} {{custom-html name="extraNavItem" tagName="li"}} {{!- this is done to avoid DIV in the UL, originally {{plugin-outlet name="extra-nav-item"}} diff --git a/app/assets/javascripts/discourse/templates/components/queued-post.hbs b/app/assets/javascripts/discourse/templates/components/queued-post.hbs index e0bbe42ac7..de2709ca4a 100644 --- a/app/assets/javascripts/discourse/templates/components/queued-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/queued-post.hbs @@ -10,8 +10,8 @@ {{#user-link user=post.user}} {{post.user.username}} {{/user-link}} - {{#if post.user.blocked}} - {{d-icon "ban" title="user.blocked_tooltip"}} + {{#if post.user.silenced}} + {{d-icon "ban" title="user.silenced_tooltip"}} {{/if}}
    diff --git a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs index c1c1914b64..f53a0e0c50 100644 --- a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs +++ b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs @@ -13,7 +13,7 @@
    - {{category-selector categories=searchedTerms.category single="true" canReceiveUpdates="true"}} + {{search-advanced-category-chooser value=searchedTerms.category}}
    @@ -74,9 +74,9 @@
    -
    +
    {{combo-box id="postTime" valueAttribute="value" content=postTimeOptions value=searchedTerms.time.when}} - {{input type="date" value=searchedTerms.time.days class="input-small" id='search-post-date'}} + {{date-picker value=searchedTerms.time.days id="search-post-date"}}
    diff --git a/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs b/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs index a4dcf9987a..3af1dba017 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs @@ -60,6 +60,7 @@ {{plugin-outlet name="topic-footer-main-buttons-before-create" args=(hash topic=topic) + tagName="" connectorTagName="span"}} {{#if topic.details.can_create_post}} @@ -72,13 +73,15 @@ {{plugin-outlet name="after-topic-footer-main-buttons" args=(hash topic=topic) + tagName="" connectorTagName="span"}} - {{pinned-button topic=topic}} + {{pinned-button pinned=topic.pinned topic=topic}}
    -{{topic-notifications-button topic=topic}} +{{topic-notifications-button notificationLevel=topic.details.notification_level topic=topic}} {{plugin-outlet name="after-topic-footer-buttons" args=(hash topic=topic) + tagName="" connectorTagName="span"}} diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index 07c4effa3e..f8e0323129 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -16,7 +16,7 @@

    - {{if nameFirst user.name username}} {{user-status user currentUser=currentUser}} + {{if nameFirst user.name (format-username username)}} {{user-status user currentUser=currentUser}}

    {{#unless nameFirst}} @@ -31,6 +31,10 @@

    {{user.title}}

    {{/if}} + {{#if user.staged}} +

    {{i18n 'user.staged'}}

    + {{/if}} + {{plugin-outlet name="user-card-post-names" args=(hash user=user) tagName='div'}}
    @@ -118,6 +122,23 @@

    {{i18n 'last_post'}} {{format-date user.last_posted_at leaveAgo="true"}}

    {{/if}}

    {{i18n 'joined'}} {{format-date user.created_at leaveAgo="true"}}

    +

    + {{i18n 'time_read'}} + {{format-duration user.time_read}} + {{#if showRecentTimeRead}} + ({{i18n 'time_read_recently' time_read=recentTimeRead}}) + {{/if}} +

    + {{#if showCheckEmail}} + + {{/if}} {{plugin-outlet name="user-card-metadata" args=(hash user=user)}}
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/user-info.hbs b/app/assets/javascripts/discourse/templates/components/user-info.hbs index 8270000cc2..ef2e2c0b6d 100644 --- a/app/assets/javascripts/discourse/templates/components/user-info.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-info.hbs @@ -4,7 +4,7 @@
    {{unbound user.title}}
    diff --git a/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs b/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs index 9c57a17212..e4e49ecbc1 100644 --- a/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs @@ -27,7 +27,13 @@

    {{actionDescription}}

    {{/if}} -

    {{{item.excerpt}}}

    +

    + {{#if item.expandedExcerpt}} + {{{item.expandedExcerpt}}} + {{else}} + {{{item.excerpt}}} + {{/if}} +

    {{#each item.children as |child|}}
    {{!-- DEPRECATED: 'child-actions' class --}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 98ad6257f1..736d7237c5 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -1,11 +1,11 @@ -{{#composer-body - composer=model - openIfDraft="openIfDraft" - typed="typed" - cancelled="cancelled" - save="save"}} +{{#composer-body composer=model + showPreview=showPreview + openIfDraft="openIfDraft" + typed="typed" + cancelled="cancelled" + save="save"}} + {{#if visible}} -
    {{#if showPopupMenu}} {{#popup-menu visible=optionsVisible hide="hideOptions" title="composer.options"}} @@ -19,134 +19,171 @@ {{/popup-menu}} {{/if}} - {{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}} + {{composer-messages composer=model + messageCount=messageCount + addLinkLookup="addLinkLookup"}} -
    - {{composer-toggles - composeState=model.composeState - toggleComposer=(action "toggle") - toggleToolbar=(action "toggleToolbar")}} - - {{#if model.viewOpen}} -
    -
    - {{plugin-outlet name="composer-open" args=(hash model=model)}} - -
    + {{#if model.viewOpen}} +
    +
    + {{plugin-outlet name="composer-open" args=(hash model=model)}} +
    +
    {{{model.actionTitle}}} + {{#unless site.mobileView}} - {{#if whisperOrUnlistTopicText}} - ({{whisperOrUnlistTopicText}}) - {{/if}} + {{#if whisperOrUnlistTopicText}} + ({{whisperOrUnlistTopicText}}) + {{/if}} {{/unless}} {{#if canEdit}} {{#if showEditReason}} -
    - {{text-field autofocus="true" value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}} -
    + {{text-field autofocus="true" value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}} {{else}} {{i18n 'composer.show_edit_reason'}} {{/if}} {{/if}}
    + {{composer-toggles composeState=model.composeState + toggleComposer=(action "toggle") + toggleToolbar=(action "toggleToolbar")}} +
    - {{#if model.canEditTitle}} -
    - {{#if model.creatingPrivateMessage}} - {{composer-user-selector topicId=topicModel.id - usernames=model.targetUsernames - hasGroups=model.hasTargetGroups - focusTarget=focusTarget}} - {{#if showWarning}} -
    - -
    - {{/if}} - {{/if}} - - {{composer-title composer=model lastValidatedAt=lastValidatedAt focusTarget=focusTarget}} - - {{#if model.showCategoryChooser}} -
    - {{category-chooser value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}} - {{popup-input-tip validation=categoryValidation}} -
    - {{#if model.archetype.hasOptions}} - {{d-button action="showOptions" label="topic.options"}} - {{/if}} + {{#if model.canEditTitle}} + {{#if model.creatingPrivateMessage}} +
    + {{composer-user-selector topicId=topicModel.id + usernames=model.targetUsernames + hasGroups=model.hasTargetGroups + focusTarget=focusTarget + class="users-input"}} + {{#if showWarning}} + {{/if}}
    {{/if}} - {{plugin-outlet name="composer-fields" args=(hash model=model)}} -
    - {{composer-editor topic=topic - composer=model - lastValidatedAt=lastValidatedAt - canWhisper=canWhisper - showPopupMenu=showPopupMenu - draftStatus=model.draftStatus - isUploading=isUploading - groupsMentioned="groupsMentioned" - cannotSeeMention="cannotSeeMention" - importQuote="importQuote" - showOptions="showOptions" - hideOptions="hideOptions" - optionsVisible=optionsVisible - showToolbar=showToolbar - showUploadSelector="showUploadSelector" - afterRefresh="afterRefresh"}} +
    - {{#if currentUser}} -
    - {{plugin-outlet name="composer-fields-below" args=(hash model=model)}} + {{composer-title composer=model lastValidatedAt=lastValidatedAt focusTarget=focusTarget}} + + {{#if model.showCategoryChooser}} +
    + {{category-chooser value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}} + {{popup-input-tip validation=categoryValidation}} +
    + {{#if model.archetype.hasOptions}} + {{d-button action="showOptions" label="topic.options"}} + {{/if}} + {{/if}} {{#if canEditTags}} {{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}} {{/if}} - {{composer-save-button - action=(action "save") - icon=model.saveIcon - label=model.saveLabel - disableSubmit=disableSubmit}} - {{i18n 'cancel'}} - {{#if site.mobileView}} +
    + {{/if}} + + {{plugin-outlet name="composer-fields" args=(hash model=model)}} + +
    + + {{composer-editor topic=topic + composer=model + lastValidatedAt=lastValidatedAt + canWhisper=canWhisper + showPopupMenu=showPopupMenu + draftStatus=model.draftStatus + isUploading=isUploading + isCancellable=isCancellable + uploadProgress=uploadProgress + groupsMentioned="groupsMentioned" + cannotSeeMention="cannotSeeMention" + importQuote="importQuote" + showOptions="showOptions" + hideOptions="hideOptions" + optionsVisible=optionsVisible + togglePreview="togglePreview" + showToolbar=showToolbar + showUploadSelector="showUploadSelector" + afterRefresh="afterRefresh"}} + +
    + {{plugin-outlet name="composer-fields-below" args=(hash model=model)}} + +
    + {{composer-save-button action=(action "save") + icon=model.saveIcon + label=model.saveLabel + disableSubmit=disableSubmit}} + + {{i18n 'cancel'}} + + {{#if site.mobileView}} {{#if whisperOrUnlistTopic}} {{d-icon "eye-slash"}} {{/if}} - {{/if}} -
    - {{/if}} -
    - {{else}} -
    -
    -
    - {{#if model.createdPost}} - {{i18n 'composer.saved'}} - {{else}} - {{i18n 'composer.saving'}} {{loading-spinner size="small"}} - {{/if}} -
    -
    - {{#if model.topic}} - {{d-icon "mail-forward"}} {{{draftTitle}}} - {{else}} - {{i18n "composer.saved_draft"}} - {{/if}} + {{/if}} + + + {{#if isUploading}} +
    + {{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} + {{uploadProgress}}% + {{#if isCancellable}} + {{d-icon "times"}} + {{/if}} +
    + {{/if}} +
    + {{model.draftStatus}}
    -
    - {{/if}} -
    -
    +
    + {{#if site.mobileView}} + {{i18n 'upload'}} + + {{#if showPreview}} + {{d-button action='togglePreview' class='hide-preview' label='composer.hide_preview'}} + {{/if}} + {{else}} + {{{toggleText}}} + {{/if}} + +
    +
    +
    + + {{else}} +
    + {{#if model.createdPost}} + {{i18n 'composer.saved'}} + {{else}} + {{i18n 'composer.saving'}} {{loading-spinner size="small"}} + {{/if}} +
    + +
    + {{#if model.topic}} + {{d-icon "mail-forward"}} {{{draftTitle}}} + {{else}} + {{i18n "composer.saved_draft"}} + {{/if}} +
    + + {{composer-toggles composeState=model.composeState + toggleComposer=(action "toggle") + toggleToolbar=(action "toggleToolbar")}} + + {{/if}} + {{/if}} + {{/composer-body}} diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 1ce6a3f4c8..163aac03fc 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -82,7 +82,10 @@
    - {{category-link result.topic.category}} + {{#if result.topic.category.parentCategory}} + {{category-link result.topic.category.parentCategory}} + {{/if}} + {{category-link result.topic.category hideParent=true}} {{#each result.topic.tags as |tag|}} {{discourse-tag tag}} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/group-activity-posts.hbs b/app/assets/javascripts/discourse/templates/group-activity-posts.hbs index 298edaff83..061160d1ed 100644 --- a/app/assets/javascripts/discourse/templates/group-activity-posts.hbs +++ b/app/assets/javascripts/discourse/templates/group-activity-posts.hbs @@ -1,7 +1,7 @@ -{{#load-more selector=".user-stream .user-stream-item" action=(action "loadMore")}} -
    +{{#load-more selector=".group-post" action=(action "loadMore")}} +
    {{#each model as |post|}} - {{group-post post=post}} + {{group-post post=post class="group-post"}} {{else}}
    {{i18n emptyText}}
    {{/each}} diff --git a/app/assets/javascripts/discourse/templates/group-edit.hbs b/app/assets/javascripts/discourse/templates/group-edit.hbs index 9f8624f13a..b149a4ad7f 100644 --- a/app/assets/javascripts/discourse/templates/group-edit.hbs +++ b/app/assets/javascripts/discourse/templates/group-edit.hbs @@ -33,8 +33,7 @@ diff --git a/app/assets/javascripts/discourse/templates/group.hbs b/app/assets/javascripts/discourse/templates/group.hbs index 777c0ea5c2..909c3f6e15 100644 --- a/app/assets/javascripts/discourse/templates/group.hbs +++ b/app/assets/javascripts/discourse/templates/group.hbs @@ -1,3 +1,5 @@ +{{plugin-outlet name="before-group-container" args=(hash group=model)}} +
    @@ -21,7 +23,7 @@
    {{#if model.bio_cooked}} -
    +

    {{{model.bio_cooked}}}

    @@ -31,17 +33,7 @@
    - {{#mobile-nav class='group-nav' desktopClass="nav nav-pills" currentPath=application.currentPath}} - {{#each getTabs as |tab|}} -
  • - {{#link-to tab.location model title=tab.message class=tab.name}} - {{#if tab.icon}}{{d-icon tab.icon}}{{/if}} - {{tab.message}} - {{#if tab.count}}({{tab.count}}){{/if}} - {{/link-to}} -
  • - {{/each}} - {{/mobile-nav}} + {{group-navigation group=model currentPath=application.currentPath tabs=tabs}} {{#if displayGroupMessageButton}} {{d-button diff --git a/app/assets/javascripts/discourse/templates/group/activity.hbs b/app/assets/javascripts/discourse/templates/group/activity.hbs index d03a14e84f..08df8ddb57 100644 --- a/app/assets/javascripts/discourse/templates/group/activity.hbs +++ b/app/assets/javascripts/discourse/templates/group/activity.hbs @@ -1,22 +1,12 @@
    {{#mobile-nav class='group-activity-nav' desktopClass="pull-left nav nav-stacked" currentPath=application.currentPath}} -
  • - {{#link-to 'group.activity.posts'}}{{i18n 'groups.posts'}}{{/link-to}} -
  • - -
  • - {{#link-to 'group.activity.topics'}}{{i18n 'groups.topics'}}{{/link-to}} -
  • - -
  • - {{#link-to 'group.activity.mentions'}}{{i18n 'groups.mentions'}}{{/link-to}} -
  • - + {{group-activity-filter filter="posts" categoryId=category_id}} + {{group-activity-filter filter="topics" categoryId=category_id}} + {{group-activity-filter filter="mentions" categoryId=category_id}} {{#if showGroupMessages}} -
  • - {{#link-to 'group.activity.messages'}}{{i18n 'groups.messages'}}{{/link-to}} -
  • + {{group-activity-filter filter="messages"}} {{/if}} + {{plugin-outlet name="below-group-activity-nav" tagName='' connectorTagName='li'}} {{/mobile-nav}}
    diff --git a/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs b/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs index f52a6b16ca..b510a920c7 100644 --- a/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs @@ -1,13 +1,13 @@ -
  • - - {{selectedNavItem.displayName}} - {{d-icon "caret-down"}} +
  • {{#if expanded}}
      {{#each navItems as |navItem|}} - {{navigation-item content=navItem filterMode=filterMode}} + {{navigation-item content=navItem filterMode=filterMode category=category}} {{/each}}
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs b/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs index a01f45c76b..f974216e93 100644 --- a/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs +++ b/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs @@ -1,33 +1,34 @@ {{#d-modal-body title="user.change_avatar.title" class="avatar-selector"}} -
    -
    - {{radio-button id="system-avatar" name="avatar" value="system" selection=selected}} - -
    -
    - {{radio-button id="gravatar" name="avatar" value="gravatar" selection=selected}} - - {{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}} -
    - {{#if allowAvatarUpload}} -
    - {{radio-button id="uploaded-avatar" name="avatar" value="uploaded" selection=selected}} - - {{avatar-uploader user_id=id - uploadedAvatarTemplate=custom_avatar_template - uploadedAvatarId=custom_avatar_upload_id - uploading=uploading - done="uploadComplete"}} -
    +
    + {{radio-button id="system-avatar" name="avatar" value="system" selection=selected}} + +
    +
    + {{radio-button id="gravatar" name="avatar" value="gravatar" selection=selected}} + + {{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}} + {{#if gravatarFailed}} +

    {{I18n 'user.change_avatar.gravatar_failed'}}

    {{/if}}
    + {{#if allowAvatarUpload}} +
    + {{radio-button id="uploaded-avatar" name="avatar" value="uploaded" selection=selected}} + + {{avatar-uploader user_id=id + uploadedAvatarTemplate=custom_avatar_template + uploadedAvatarId=custom_avatar_upload_id + uploading=uploading + done="uploadComplete"}} +
    + {{/if}} {{/d-modal-body}}
    - + {{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
    {{i18n 'user.tracked_categories_instructions'}}
    @@ -21,13 +21,13 @@
    - + {{category-selector categories=model.watchedFirstPostCategories}}
    {{i18n 'user.watched_first_post_categories_instructions'}}
    - + {{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
    {{i18n 'user.muted_categories_instructions'}}
    diff --git a/app/assets/javascripts/discourse/templates/preferences/interface.hbs b/app/assets/javascripts/discourse/templates/preferences/interface.hbs index 8e8d5852e9..f6db98b840 100644 --- a/app/assets/javascripts/discourse/templates/preferences/interface.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/interface.hbs @@ -23,6 +23,13 @@ {{/if}} +
    + +
    + {{combo-box content=userSelectableHome valueAttribute="value" value=model.user_option.homepage_id}} +
    +
    +
    diff --git a/app/assets/javascripts/discourse/templates/preferences/profile.hbs b/app/assets/javascripts/discourse/templates/preferences/profile.hbs index c94a78775d..39686f2b12 100644 --- a/app/assets/javascripts/discourse/templates/preferences/profile.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/profile.hbs @@ -1,7 +1,7 @@ {{#if canChangeBio}}
    -
    +
    {{d-editor value=model.bio_raw}}
    diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index f26c41d641..f5958d118a 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -18,20 +18,26 @@ {{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}} {{#if showCategoryChooser}} -
    {{category-chooser class="small" value=buffered.category_id}} {{/if}} {{#if canEditTags}} -
    {{tag-chooser tags=buffered.tags categoryId=buffered.category_id}} {{/if}} {{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}} -
    - {{d-button action="finishedEditingTopic" class="btn-primary btn-small submit-edit" icon="check"}} - {{d-button action="cancelEditingTopic" class="btn-small cancel-edit" icon="times"}} +
    + {{d-button action="finishedEditingTopic" class="btn-primary btn-small submit-edit" icon="check"}} + {{d-button action="cancelEditingTopic" class="btn-small cancel-edit" icon="times"}} + + {{#if canRemoveTopicFeaturedLink}} + + {{d-icon "times-circle"}} + {{featuredLinkDomain}} + + {{/if}} +
    {{else}}

    {{#unless model.is_warning}} @@ -85,6 +91,7 @@ {{topic-timeline topic=model + notificationLevel=model.details.notification_level prevEvent=info.prevEvent fullscreen=info.topicProgressExpanded enteredIndex=enteredIndex diff --git a/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs b/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs index 54947fbdaa..17a4df882e 100644 --- a/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs +++ b/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs @@ -8,6 +8,6 @@ {{i18n "topic.unsubscribe.change_notification_state"}}

    - {{topic-notifications-button topic=model}} + {{topic-notifications-button notificationLevel=model.details.notification_level topic=model}}

    diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index fab58ece1f..5f8dc6d18a 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -71,7 +71,7 @@ {{format-date invite.user.last_seen_at}} {{number invite.user.topics_entered}} {{number invite.user.posts_read_count}} - {{{unbound invite.user.time_read}}} + {{{format-duration invite.user.time_read}}} {{{unbound invite.user.days_visited}}} / {{{unbound invite.user.days_since_created}}} diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 17677dd3d1..9307c32102 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -61,7 +61,7 @@
    -

    {{if nameFirst model.name model.username}} {{user-status model currentUser=currentUser}}

    +

    {{if nameFirst model.name (format-username model.username)}} {{user-status model currentUser=currentUser}}

    {{#if nameFirst}}{{model.username}}{{else}}{{model.name}}{{/if}}

    {{#if model.title}}

    {{model.title}}

    diff --git a/app/assets/javascripts/discourse/templates/user/activity.hbs b/app/assets/javascripts/discourse/templates/user/activity.hbs index 3e317daa2f..f546aff2f4 100644 --- a/app/assets/javascripts/discourse/templates/user/activity.hbs +++ b/app/assets/javascripts/discourse/templates/user/activity.hbs @@ -1,5 +1,5 @@ {{#d-section pageClass="user-activity" class="user-navigation" scrollTop="false"}} - {{#mobile-nav class='activity-nav' desktopClass='action-list activity-list nav-stacked' currentPath=currentPath}} + {{#mobile-nav class='activity-nav' desktopClass='action-list activity-list nav-stacked' currentPath=application.currentPath}}
  • {{#link-to 'userActivity.index'}}{{i18n 'user.filters.all'}}{{/link-to}}
  • @@ -12,7 +12,7 @@
  • {{#link-to 'userActivity.likesGiven'}}{{i18n 'user_action_groups.1'}}{{/link-to}}
  • - {{#if showBookmarks}} + {{#if user.showBookmarks}}
  • {{#link-to 'userActivity.bookmarks'}}{{i18n 'user_action_groups.3'}}{{/link-to}}
  • @@ -22,7 +22,7 @@ args=(hash model=model)}} {{/mobile-nav}} - {{#if viewingSelf}} + {{#if canDownloadPosts}}
    {{d-button action="exportUserArchive" label="user.download_archive.button_text" icon="download"}}
    diff --git a/app/assets/javascripts/discourse/templates/user/messages.hbs b/app/assets/javascripts/discourse/templates/user/messages.hbs index 3e19ee722a..5a78828233 100644 --- a/app/assets/javascripts/discourse/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/templates/user/messages.hbs @@ -73,7 +73,7 @@ {{/if}} {{#if isGroup}} - {{group-notifications-button group=group user=model}} + {{group-notifications-button value=group.group_user.notification_level group=group user=model}} {{/if}}
    diff --git a/app/assets/javascripts/discourse/templates/user/summary.hbs b/app/assets/javascripts/discourse/templates/user/summary.hbs index 97c7cf9c4e..695cbf1c01 100644 --- a/app/assets/javascripts/discourse/templates/user/summary.hbs +++ b/app/assets/javascripts/discourse/templates/user/summary.hbs @@ -6,7 +6,15 @@ {{user-stat value=model.days_visited label="user.summary.days_visited"}}
  • - {{user-stat value=model.time_read label="user.summary.time_read" type="string"}} + {{user-stat value=timeRead label="user.summary.time_read" type="string"}} +
  • + {{#if showRecentTimeRead}} +
  • + {{user-stat value=recentTimeRead label="user.summary.recent_time_read" type="string"}} +
  • + {{/if}} +
  • + {{user-stat value=model.topics_entered label="user.summary.topics_entered"}}
  • {{user-stat value=model.posts_read_count label="user.summary.posts_read"}} diff --git a/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 b/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 index 0e8212f35a..c784fea47a 100644 --- a/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 +++ b/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 @@ -30,7 +30,7 @@ createWidget('small-user-list', { let description = null; if (atts.description) { - description = I18n.t(atts.description, { icons: '' }); + description = I18n.t(atts.description, { count: atts.count }); } // oddly post_url is on the user diff --git a/app/assets/javascripts/discourse/widgets/component-connector.js.es6 b/app/assets/javascripts/discourse/widgets/component-connector.js.es6 new file mode 100644 index 0000000000..5f01effceb --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/component-connector.js.es6 @@ -0,0 +1,51 @@ +export default class ComponentConnector { + constructor(widget, componentName, opts, trackedProperties) { + this.widget = widget; + this.opts = opts; + this.componentName = componentName; + this.trackedProperties = trackedProperties || []; + } + + init() { + const $elem = $('
    '); + const elem = $elem[0]; + const { opts, widget, componentName } = this; + + Ember.run.next(() => { + const mounted = widget._findView(); + + const view = widget + .register + .lookupFactory(`component:${componentName}`) + .create(opts); + + if (Ember.setOwner) { + Ember.setOwner(view, Ember.getOwner(mounted)); + } + + mounted._connected.push(view); + view.renderer.appendTo(view, $elem[0]); + }); + + return elem; + } + + update(prev) { + // mutated external properties might not correctly update the underlying component + // in this case we can define trackedProperties, if different from previous + // state we will re-init the whole component, be careful when using this + // to not track a property which would be updated too often (on scroll for example) + let shouldInit = false; + this.trackedProperties.forEach(prop => { + if (prev.opts[prop] !== this.opts[prop]) { + shouldInit = true; + } + }); + + if (shouldInit) return this.init(); + + return null; + } +} + +ComponentConnector.prototype.type = 'Widget'; diff --git a/app/assets/javascripts/discourse/widgets/component_connector.js.es6 b/app/assets/javascripts/discourse/widgets/component_connector.js.es6 deleted file mode 100644 index 7484fad179..0000000000 --- a/app/assets/javascripts/discourse/widgets/component_connector.js.es6 +++ /dev/null @@ -1,35 +0,0 @@ -export default class ComponentConnector { - constructor(widget, componentName, opts) { - this.widget = widget; - this.opts = opts; - this.componentName = componentName; - } - - init() { - const $elem = $('
    '); - const elem = $elem[0]; - const { opts, widget, componentName } = this; - - Ember.run.next(() => { - const mounted = widget._findView(); - - const view = widget - .register - .lookupFactory(`component:${componentName}`) - .create(opts); - - if (Ember.setOwner) { - Ember.setOwner(view, Ember.getOwner(mounted)); - } - - mounted._connected.push(view); - view.renderer.appendTo(view, $elem[0]); - }); - - return elem; - } - - update() { } -} - -ComponentConnector.prototype.type = 'Widget'; diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 2058a4b882..77e8b33186 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -35,7 +35,9 @@ export default createWidget('hamburger-menu', { settings: { showCategories: true, - maxWidth: 300 + maxWidth: 300, + showFAQ: true, + showAbout: true }, adminLinks() { @@ -130,7 +132,7 @@ export default createWidget('hamburger-menu', { const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; const isStaff = Discourse.User.currentProp('staff'); - const categories = Discourse.Category.list().reject((c) => { + const categories = this.site.get('categoriesList').reject((c) => { if (c.get('parentCategory.show_subcategory_list')) { return true; } if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; } return false; @@ -141,9 +143,11 @@ export default createWidget('hamburger-menu', { footerLinks(prioritizeFaq, faqUrl) { const links = []; - links.push({ route: 'about', className: 'about-link', label: 'about.simple_title' }); + if (this.settings.showAbout) { + links.push({ route: 'about', className: 'about-link', label: 'about.simple_title' }); + } - if (!prioritizeFaq) { + if (this.settings.showFAQ && !prioritizeFaq) { links.push({ href: faqUrl, className: 'faq-link', label: 'faq' }); } @@ -171,7 +175,10 @@ export default createWidget('hamburger-menu', { faqUrl = Discourse.getURL('/faq'); } - const prioritizeFaq = this.currentUser && !this.currentUser.read_faq; + const prioritizeFaq = this.settings.showFAQ && + this.currentUser && + !this.currentUser.read_faq; + if (prioritizeFaq) { results.push(this.attach('menu-links', { name: 'faq-link', heading: true, contents: () => { return this.attach('priority-faq-link', { href: faqUrl }); diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 index 4c4b1e147f..db166e6659 100644 --- a/app/assets/javascripts/discourse/widgets/header.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -41,7 +41,9 @@ createWidget('header-notifications', { action: attrs.action, className: 'badge-notification unread-notifications', rawLabel: unreadNotifications, - omitSpan: true + omitSpan: true, + title: "notifications.tooltip.regular", + titleOptions: {count: unreadNotifications} })); } @@ -62,7 +64,9 @@ createWidget('header-notifications', { action: attrs.action, className: 'badge-notification unread-private-messages', rawLabel: unreadPMs, - omitSpan: true + omitSpan: true, + title: "notifications.tooltip.message", + titleOptions: {count: unreadPMs} })); } diff --git a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 index f2878b5f12..83f0c368fa 100644 --- a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 @@ -4,7 +4,7 @@ import { createWidget } from 'discourse/widgets/widget'; import DiscourseURL from 'discourse/lib/url'; import { h } from 'virtual-dom'; import { emojiUnescape } from 'discourse/lib/text'; -import { postUrl, escapeExpression } from 'discourse/lib/utilities'; +import { postUrl, escapeExpression, formatUsername } from 'discourse/lib/utilities'; import { setTransientHeader } from 'discourse/lib/ajax'; import { userPath } from 'discourse/lib/url'; import { iconNode } from 'discourse-common/lib/icon-library'; @@ -79,11 +79,11 @@ createWidget('notification-item', { return I18n.t(scope, { count, group_name }); } - const username = data.display_username; + const username = formatUsername(data.display_username); const description = this.description(); if (notificationType === LIKED_TYPE && data.count > 1) { const count = data.count - 2; - const username2 = data.username2; + const username2 = formatUsername(data.username2); if (count===0) { return I18n.t('notifications.liked_2', {description, username, username2}); } else { diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index 80ab77838e..7b4483c28a 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -30,10 +30,10 @@ function registerButton(name, builder) { } export function buildButton(name, widget) { - let { attrs, state, siteSettings } = widget; + let { attrs, state, siteSettings, settings } = widget; let builder = _builders[name]; if (builder) { - let button = builder(attrs, state, siteSettings); + let button = builder(attrs, state, siteSettings, settings); if (button && !button.id) { button.id = name; } @@ -166,7 +166,7 @@ registerButton('share', attrs => { }; }); -registerButton('reply', attrs => { +registerButton('reply', (attrs, state, siteSettings, postMenuSettings) => { const args = { action: 'replyToPost', title: 'post.controls.reply', @@ -176,7 +176,7 @@ registerButton('reply', attrs => { if (!attrs.canCreatePost) { return; } - if (!attrs.mobileView) { + if (postMenuSettings.showReplyTitleOnMobile || !attrs.mobileView) { args.label = 'topic.reply.title'; } @@ -233,7 +233,8 @@ export default createWidget('post-menu', { settings: { collapseButtons: true, - buttonType: 'flat-button' + buttonType: 'flat-button', + showReplyTitleOnMobile: false }, defaultState() { @@ -353,11 +354,13 @@ export default createWidget('post-menu', { const contents = [ h('nav.post-controls.clearfix', postControls) ]; if (state.likedUsers.length) { + const remaining = state.total - state.likedUsers.length; contents.push(this.attach('small-user-list', { users: state.likedUsers, - addSelf: attrs.liked, + addSelf: attrs.liked && remaining === 0, listClassName: 'who-liked', - description: 'post.actions.people.like' + description: remaining > 0 ? 'post.actions.people.like_capped' : 'post.actions.people.like', + count: remaining })); } @@ -413,6 +416,7 @@ export default createWidget('post-menu', { return this.store.find('post-action-user', { id: attrs.id, post_action_type_id: LIKE_ACTION }).then(users => { state.likedUsers = users.map(avatarAtts); + state.total = users.totalRows; }); }, diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index 4b5acec52d..6cc9f147bf 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -6,7 +6,7 @@ import { transformBasicPost } from 'discourse/lib/transform-post'; import { h } from 'virtual-dom'; import DiscourseURL from 'discourse/lib/url'; import { dateNode } from 'discourse/helpers/node'; -import { translateSize, avatarUrl } from 'discourse/lib/utilities'; +import { translateSize, avatarUrl, formatUsername } from 'discourse/lib/utilities'; export function avatarImg(wanted, attrs) { const size = translateSize(wanted); @@ -14,7 +14,7 @@ export function avatarImg(wanted, attrs) { // We won't render an invalid url if (!url || url.length === 0) { return; } - const title = attrs.username; + const title = formatUsername(attrs.username); const properties = { attributes: { alt: '', width: size, height: size, src: Discourse.getURLWithCDN(url), title }, diff --git a/app/assets/javascripts/discourse/widgets/poster-name.js.es6 b/app/assets/javascripts/discourse/widgets/poster-name.js.es6 index 2bf64d738d..363f769101 100644 --- a/app/assets/javascripts/discourse/widgets/poster-name.js.es6 +++ b/app/assets/javascripts/discourse/widgets/poster-name.js.es6 @@ -1,6 +1,7 @@ import { iconNode } from 'discourse-common/lib/icon-library'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; +import { formatUsername } from 'discourse/lib/utilities'; function sanitizeName(name){ return name.toLowerCase().replace(/[\s_-]/g,''); @@ -17,10 +18,11 @@ export default createWidget('poster-name', { }, userLink(attrs, text) { - return h('a', { attributes: { - href: attrs.usernameUrl, - 'data-user-card': attrs.username - } }, text); + return h( + 'a', + { attributes: { href: attrs.usernameUrl, 'data-user-card': attrs.username } }, + formatUsername(text) + ); }, html(attrs) { diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 index 05aa2d705b..705f9e685b 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -5,7 +5,7 @@ import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; import { iconNode } from 'discourse-common/lib/icon-library'; import highlightText from 'discourse/lib/highlight-text'; -import { escapeExpression } from 'discourse/lib/utilities'; +import { escapeExpression, formatUsername } from 'discourse/lib/utilities'; class Highlighted extends RawHtml { constructor(html, term) { @@ -57,7 +57,15 @@ createSearchResult({ type: 'user', linkField: 'path', builder(u) { - return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', h('span.user-results', h('b', u.username)), ' ', h('span.user-results', u.name ? u.name : '') ]; + return [ + avatarImg('small', { + template: u.avatar_template, username: u.username + }), + ' ', + h('span.user-results', h('b', formatUsername(u.username))), + ' ', + h('span.user-results', u.name ? u.name : '') + ]; } }); diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index 6cfd0f55f0..fbf6074e0a 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -168,17 +168,16 @@ createWidget('topic-map-expanded', { const result = [avatars]; if (attrs.topicLinks) { - const toShow = state.allLinksShown ? attrs.topicLinks : attrs.topicLinks.slice(0, LINKS_SHOWN); - const links = toShow.map(l => { + const links = toShow.map(l => { let host = ''; + if (l.title && l.title.length) { - const domain = l.domain; - if (domain && domain.length) { - const s = domain.split('.'); - if (s[0] === 'www') s.shift(); - host = h('span.domain', s.join('.')); + const rootDomain = l.root_domain; + + if (rootDomain && rootDomain.length) { + host = h('span.domain', rootDomain); } } diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 index 2fb698a38c..0b4d32661a 100644 --- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 @@ -1,5 +1,5 @@ import { createWidget } from 'discourse/widgets/widget'; -import ComponentConnector from 'discourse/widgets/component_connector'; +import ComponentConnector from 'discourse/widgets/component-connector'; import { h } from 'virtual-dom'; import { relativeAge } from 'discourse/lib/formatter'; import { iconNode } from 'discourse-common/lib/icon-library'; @@ -291,7 +291,7 @@ createWidget('timeline-footer-controls', { html(attrs) { const controls = []; - const { currentUser, fullScreen, topic } = attrs; + const { currentUser, fullScreen, topic, notificationLevel } = attrs; if (currentUser && !fullScreen) { if (topic.get('details.can_create_post')) { @@ -315,12 +315,13 @@ createWidget('timeline-footer-controls', { if (currentUser) { controls.push(new ComponentConnector(this, - 'topic-notifications-button', + 'topic-notifications-options', { + value: notificationLevel, topic, - appendReason: false, showFullTitle: false - } + }, + ["value"] )); } diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 index 6ba446b8be..302264c07e 100644 --- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -1,5 +1,7 @@ import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; +import { formatUsername } from 'discourse/lib/utilities'; +import { ajax } from 'discourse/lib/ajax'; let extraGlyphs; @@ -50,7 +52,7 @@ createWidget('user-menu-links', { model: currentUser, className: 'user-activity-link', icon: 'user', - rawLabel: currentUser.username + rawLabel: formatUsername(currentUser.username) }; if (currentUser.is_anonymous) { @@ -86,6 +88,38 @@ createWidget('user-menu-links', { } }); +createWidget('user-menu-dismiss-link', { + tagName: 'div.dismiss-link', + buildKey: () => 'user-menu-dismiss-link', + + html() { + if (userNotifications.state.notifications.filterBy("read", false).length > 0) { + return h('ul.menu-links', + h('li', + this.attach('link', { + action: 'dismissNotifications', + className: 'dismiss', + icon: 'check', + label: 'user.dismiss', + title: 'user.dismiss_notifications_tooltip' + }) + ) + ); + } else { + return ''; + } + }, + + dismissNotifications() { + ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { + userNotifications.notificationsChanged(); + }); + } +}); + +let userNotifications = null, + dismissLink = null; + export default createWidget('user-menu', { tagName: 'div.user-menu', @@ -95,16 +129,26 @@ export default createWidget('user-menu', { panelContents() { const path = this.currentUser.get('path'); + userNotifications = this.attach('user-notifications', { path }); + dismissLink = this.attach('user-menu-dismiss-link'); - return [this.attach('user-menu-links', { path }), - this.attach('user-notifications', { path }), - h('div.logout-link', [ - h('ul.menu-links', - h('li', this.attach('link', { action: 'logout', - className: 'logout', - icon: 'sign-out', - label: 'user.log_out' }))) - ])]; + return [ + this.attach('user-menu-links', { path }), + userNotifications, + h('div.logout-link', [ + h('ul.menu-links', + h('li', + this.attach('link', { + action: 'logout', + className: 'logout', + icon: 'sign-out', + label: 'user.log_out' + }) + ) + ) + ]), + dismissLink + ]; }, html() { diff --git a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 index c7570479cc..a6353a995f 100644 --- a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 @@ -1,6 +1,7 @@ import { createWidget } from 'discourse/widgets/widget'; import { headerHeight } from 'discourse/components/site-header'; import { h } from 'virtual-dom'; +import DiscourseURL from 'discourse/lib/url'; export default createWidget('user-notifications', { tagName: 'div.notifications', @@ -71,11 +72,14 @@ export default createWidget('user-notifications', { const items = [notificationItems]; if (notificationItems.length > 5) { - const href = `${attrs.path}/notifications`; - items.push( - h('li.read.last.heading', - h('a', { attributes: { href } }, [I18n.t('notifications.more'), '...'])), + h('li.read.last.heading.show-all', + this.attach('button', { + title: 'notifications.more', + icon: 'chevron-down', + action: 'showAllNotifications', + className: 'btn' + })), h('hr') ); } @@ -84,5 +88,9 @@ export default createWidget('user-notifications', { } return result; + }, + + showAllNotifications() { + DiscourseURL.routeTo(`${this.attrs.path}/notifications`); } }); diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 022625168f..f5822f8fa9 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -133,7 +133,21 @@ export function reopenWidget(name, opts) { opts.html = opts.template; } - Object.keys(opts).forEach(k => existing.prototype[k] = opts[k]); + Object.keys(opts).forEach(k => { + let old = existing.prototype[k]; + + if (old) { + // Add support for `this._super()` to reopened widgets if the prototype exists in the + // base object + existing.prototype[k] = function(...args) { + let ctx = Object.create(this); + ctx._super = (...superArgs) => old.apply(this, superArgs); + return opts[k].apply(ctx, args); + }; + } else { + existing.prototype[k] = opts[k]; + } + }); return existing; } diff --git a/app/assets/javascripts/locales/i18n.js b/app/assets/javascripts/locales/i18n.js index ddf3fd55f1..ba56bc3a80 100644 --- a/app/assets/javascripts/locales/i18n.js +++ b/app/assets/javascripts/locales/i18n.js @@ -48,8 +48,7 @@ function checkExtras(origScope, sep, extras) { I18n.lookup = function(scope, options) { options = options || {}; - var lookupInitialScope = scope, - translations = this.prepareOptions(I18n.translations), + var translations = this.prepareOptions(I18n.translations), locale = options.locale || I18n.currentLocale(), messages = translations[locale] || {}, currentScope; diff --git a/app/assets/javascripts/markdown-it-bundle.js b/app/assets/javascripts/markdown-it-bundle.js index 3cd7a69f2f..eba760fd6b 100644 --- a/app/assets/javascripts/markdown-it-bundle.js +++ b/app/assets/javascripts/markdown-it-bundle.js @@ -15,3 +15,4 @@ //= require ./pretty-text/engines/discourse-markdown/html-img //= require ./pretty-text/engines/discourse-markdown/text-post-process //= require ./pretty-text/engines/discourse-markdown/image-protocol +//= require ./pretty-text/engines/discourse-markdown/inject-line-number diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/inject-line-number.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/inject-line-number.js.es6 new file mode 100644 index 0000000000..eaad9f61b6 --- /dev/null +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/inject-line-number.js.es6 @@ -0,0 +1,50 @@ +export function setup(helper) { + if (helper.getOptions().previewing) { + helper.whiteList([ + 'p.preview-sync-line', + 'p[data-line-number]', + 'h1.preview-sync-line', + 'h1[data-line-number]', + 'h2.preview-sync-line', + 'h2[data-line-number]', + 'h3.preview-sync-line', + 'h3[data-line-number]', + 'h4.preview-sync-line', + 'h4[data-line-number]', + 'h5.preview-sync-line', + 'h5[data-line-number]', + 'h6.preview-sync-line', + 'h6[data-line-number]', + 'blockquote.preview-sync-line', + 'blockquote[data-line-number]', + 'hr.preview-sync-line', + 'hr[data-line-number]', + 'ul.preview-sync-line', + 'ul[data-line-number]', + 'ol.preview-sync-line', + 'ol[data-line-number]', + ]); + + helper.registerPlugin(md => { + const injectLineNumber = (tokens, index, options, env, self) => { + let line; + const token = tokens[index]; + + if (token.map && token.level === 0) { + line = token.map[0]; + token.attrJoin('class', 'preview-sync-line'); + token.attrSet('data-line-number', String(line)); + } + + return self.renderToken(tokens, index, options, env, self); + }; + + md.renderer.rules.paragraph_open = injectLineNumber; + md.renderer.rules.heading_open = injectLineNumber; + md.renderer.rules.blockquote_open = injectLineNumber; + md.renderer.rules.hr = injectLineNumber; + md.renderer.rules.ordered_list_open = injectLineNumber; + md.renderer.rules.bullet_list_open = injectLineNumber; + }); + } +} diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 index 54a5284372..7314bc1f98 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 @@ -1,7 +1,6 @@ function addMention(buffer, matches, state) { let username = matches[1] || matches[2]; - let mentionLookup = state.md.options.discourse.mentionLookup; - let getURL = state.md.options.discourse.getURL; + let { getURL, mentionLookup, formatUsername } = state.md.options.discourse; let type = mentionLookup && mentionLookup(username); @@ -25,6 +24,9 @@ function addMention(buffer, matches, state) { } buffer.push(token); + if (formatUsername) { + username = formatUsername(username); + } token = new state.Token('text', '', 0); token.content = '@'+username; diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 index 70ce99c6fc..3a0523bef6 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 @@ -3,13 +3,13 @@ import { performEmojiUnescape } from 'pretty-text/emoji'; const rule = { tag: 'quote', - before: function(state, tagInfo) { + before(state, tagInfo) { const attrs = tagInfo.attrs; let options = state.md.options.discourse; let quoteInfo = attrs['_default']; - let username, postNumber, topicId, avatarImg, full; + let username, postNumber, topicId, avatarImg, primaryGroupName, full, displayName; if (quoteInfo) { let split = quoteInfo.split(/\,\s*/); @@ -34,9 +34,36 @@ const rule = { } } + if (options.lookupAvatarByPostNumber) { + // client-side, we can retrieve the avatar from the post + avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId); + } else if (options.lookupAvatar) { + // server-side, we need to lookup the avatar from the username + avatarImg = options.lookupAvatar(username); + } - let token = state.push('bbcode_open', 'aside', 1); - token.attrs = [['class', 'quote']]; + if (options.lookupPrimaryUserGroupByPostNumber) { + // client-side, we can retrieve the primary user group from the post + primaryGroupName = options.lookupPrimaryUserGroupByPostNumber(postNumber, topicId); + } else if (options.lookupPrimaryUserGroup) { + // server-side, we need to lookup the primary user group from the username + primaryGroupName = options.lookupPrimaryUserGroup(username); + } + + if (options.formatUsername) { + displayName = options.formatUsername(username); + } else { + displayName = username; + } + + let token = state.push('bbcode_open', 'aside', 1); + token.attrs = []; + + if (primaryGroupName && primaryGroupName.length !== 0) { + token.attrs.push(['class', `quote group-${primaryGroupName}`]); + } else { + token.attrs.push(['class', 'quote']); + } if (postNumber) { token.attrs.push(['data-post', postNumber]); @@ -50,14 +77,6 @@ const rule = { token.attrs.push(['data-full', 'true']); } - if (options.lookupAvatarByPostNumber) { - // client-side, we can retrieve the avatar from the post - avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId); - } else if (options.lookupAvatar) { - // server-side, we need to lookup the avatar from the username - avatarImg = options.lookupAvatar(username); - } - if (username) { let offTopicQuote = options.topicId && postNumber && @@ -105,7 +124,7 @@ const rule = { } } else { token = state.push('text', '', 0); - token.content = ` ${username}:`; + token.content = ` ${displayName}:`; } token = state.push('quote_header_close', 'div', -1); @@ -132,4 +151,11 @@ export function setup(helper) { }); helper.whiteList(['img[class=avatar]']); + helper.whiteList({ + custom(tag, name, value) { + if (tag === 'aside' && name === 'class') { + return !!/^quote group\-(.+)$/.exec(value); + } + } + }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 index c3760799e0..3506e66ceb 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 @@ -1,6 +1,15 @@ export function setup(helper) { - // this is built in now - // TODO: sanitizer needs fixing, does not properly support this yet + + helper.registerPlugin(md => { + + md.renderer.rules.table_open = function(){ + return '
    \n\n'; + }; + + md.renderer.rules.table_close = function(){ + return '
    \n
    '; + }; + }); // we need a custom callback for style handling helper.whiteList({ @@ -24,5 +33,6 @@ export function setup(helper) { 'tr', 'th', 'td', + 'div.md-table' ]); } diff --git a/app/assets/javascripts/pretty-text/image-short-url.js.es6 b/app/assets/javascripts/pretty-text/image-short-url.js.es6 index d815b46696..4efcefaec3 100644 --- a/app/assets/javascripts/pretty-text/image-short-url.js.es6 +++ b/app/assets/javascripts/pretty-text/image-short-url.js.es6 @@ -16,3 +16,35 @@ export function lookupUncachedUploadUrls(urls, ajax) { export function cacheShortUploadUrl(shortUrl, url) { _cache[shortUrl] = url; } + +function _loadCachedShortUrls($images) { + $images.each((idx, image) => { + let $image = $(image); + let url = lookupCachedUploadUrl($image.data('orig-src')); + if (url) { + $image.removeAttr('data-orig-src'); + if (url !== "missing") { + $image.attr('src', url); + } + } + }); +} + +function _loadShortUrls($images, ajax) { + const urls = _.map($images, img => $(img).data('orig-src')); + lookupUncachedUploadUrls(urls, ajax).then(() => _loadCachedShortUrls($images)); +} + +export function resolveAllShortUrls(ajax) { + let $shortUploadUrls = $('img[data-orig-src]'); + + if ($shortUploadUrls.length > 0) { + _loadCachedShortUrls($shortUploadUrls); + + $shortUploadUrls = $('img[data-orig-src]'); + if ($shortUploadUrls.length > 0) { + // this is carefully batched so we can do a leading debounce (trigger right away) + Ember.run.debounce(null, () => { _loadShortUrls($shortUploadUrls, ajax); }, 450, true); + } + } +} diff --git a/app/assets/javascripts/pretty-text/oneboxer.js.es6 b/app/assets/javascripts/pretty-text/oneboxer.js.es6 index ac84ca0d1a..245c87cbca 100644 --- a/app/assets/javascripts/pretty-text/oneboxer.js.es6 +++ b/app/assets/javascripts/pretty-text/oneboxer.js.es6 @@ -3,6 +3,38 @@ const loadingQueue = []; const localCache = {}; const failedCache = {}; +function resolveSize(img) { + $(img).addClass('size-resolved'); + + if (img.width > 0 && img.width === img.height) { + $(img).addClass('onebox-avatar'); + } +} + +// Detect square images and apply smaller onebox-avatar class +function applySquareGenericOnebox($elem, normalizedUrl) { + if (!$elem.hasClass('whitelistedgeneric')) { + return; + } + + let $img = $elem.find('.onebox-body img.thumbnail'); + let img = $img[0]; + + // already resolved... skip + if ($img.length !== 1 || $img.hasClass('size-resolved')) { + return; + } + + if (img.complete) { + resolveSize(img, $elem, normalizedUrl); + } else { + $img.on('load.onebox', () => { + resolveSize(img, $elem, normalizedUrl); + $img.off('load.onebox'); + }); + } +} + function loadNext(ajax) { if (loadingQueue.length === 0) { timeout = null; @@ -16,11 +48,14 @@ function loadNext(ajax) { // Retrieve the onebox return ajax("/onebox", { dataType: 'html', - data: { url, refresh, user_id: userId }, + data: { url, refresh }, cache: true }).then(html => { - localCache[normalize(url)] = html; - $elem.replaceWith(html); + let $html = $(html); + + localCache[normalize(url)] = $html; + $elem.replaceWith($html); + applySquareGenericOnebox($html, normalize(url)); }, result => { if (result && result.jqXHR && result.jqXHR.status === 429) { timeoutMs = 2000; @@ -53,7 +88,7 @@ export function load(e, refresh, ajax, userId, synchronous) { if (!refresh) { // If we have it in our cache, return it. const cached = localCache[normalize(url)]; - if (cached) return cached; + if (cached) return cached.prop('outerHTML'); // If the request failed, don't do anything const failed = failedCache[normalize(url)]; @@ -81,5 +116,6 @@ function normalize(url) { } export function lookupCache(url) { - return localCache[normalize(url)]; + const cached = localCache[normalize(url)]; + return cached && cached.prop('outerHTML'); } diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index 9c7a814940..002f489d7d 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -12,6 +12,7 @@ export function buildOptions(state) { siteSettings, getURL, lookupAvatar, + lookupPrimaryUserGroup, getTopicInfo, topicId, categoryHashtagLookup, @@ -19,12 +20,15 @@ export function buildOptions(state) { getCurrentUser, currentUser, lookupAvatarByPostNumber, + lookupPrimaryUserGroupByPostNumber, + formatUsername, emojiUnicodeReplacer, lookupInlineOnebox, lookupImageUrls, previewing, linkify, - censoredWords + censoredWords, + mentionLookup } = state; let features = { @@ -49,6 +53,7 @@ export function buildOptions(state) { getURL, features, lookupAvatar, + lookupPrimaryUserGroup, getTopicInfo, topicId, categoryHashtagLookup, @@ -56,7 +61,9 @@ export function buildOptions(state) { getCurrentUser, currentUser, lookupAvatarByPostNumber, - mentionLookup: state.mentionLookup, + lookupPrimaryUserGroupByPostNumber, + formatUsername, + mentionLookup, emojiUnicodeReplacer, lookupInlineOnebox, lookupImageUrls, diff --git a/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 b/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 deleted file mode 100644 index f39d219db6..0000000000 --- a/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 +++ /dev/null @@ -1,50 +0,0 @@ -import DropdownSelectBoxComponent from "select-box-kit/components/dropdown-select-box"; -import { iconHTML } from "discourse-common/lib/icon-library"; -import computed from "ember-addons/ember-computed-decorators"; -import { on } from "ember-addons/ember-computed-decorators"; - -export default DropdownSelectBoxComponent.extend({ - classNames: "categories-admin-dropdown", - actionNames: { create: "createCategory", reorder: "reorderCategories" }, - - @on("didReceiveAttrs") - _setComponentOptions() { - this.set("headerComponentOptions", Ember.Object.create({ - shouldDisplaySelectedName: false, - icon: `${iconHTML('bars')}${iconHTML('caret-down')}`.htmlSafe(), - })); - }, - - @computed - content() { - const items = [ - { - id: "create", - name: I18n.t("category.create"), - description: I18n.t("category.create_long"), - icon: "plus" - } - ]; - - const includeReorder = this.get("siteSettings.fixed_category_positions"); - if (includeReorder) { - items.push({ - id: "reorder", - name: I18n.t("categories.reorder.title"), - description: I18n.t("categories.reorder.title_long"), - icon: "random" - }); - } - - return items; - }, - - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - - this.sendAction(`actionNames.${value}`); - this.set("value", null); - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/category-chooser.js.es6 b/app/assets/javascripts/select-box-kit/components/category-chooser.js.es6 deleted file mode 100644 index ee6e9b158a..0000000000 --- a/app/assets/javascripts/select-box-kit/components/category-chooser.js.es6 +++ /dev/null @@ -1,131 +0,0 @@ -import ComboBoxComponent from "select-box-kit/components/combo-box"; -import { categoryBadgeHTML } from "discourse/helpers/category-link"; -import { on } from "ember-addons/ember-computed-decorators"; -import computed from "ember-addons/ember-computed-decorators"; -import PermissionType from "discourse/models/permission-type"; -import Category from "discourse/models/category"; -const { get, isNone, isEmpty } = Ember; - -export default ComboBoxComponent.extend({ - classNames: "category-chooser", - filterable: true, - castInteger: true, - allowUncategorized: null, - - filterFunction(computedContent) { - const _matchFunction = (filter, text) => { - return text.toLowerCase().indexOf(filter) > -1; - }; - - return (selectBox) => { - const filter = selectBox.get("filter").toLowerCase(); - return _.filter(computedContent, c => { - const category = Category.findById(get(c, "value")); - const text = get(c, "name"); - if (category && category.get("parentCategory")) { - const categoryName = category.get("parentCategory.name"); - return _matchFunction(filter, text) || _matchFunction(filter, categoryName); - } else { - return _matchFunction(filter, text); - } - }); - }; - }, - - @computed("rootNone", "rootNoneLabel") - none(rootNone, rootNoneLabel) { - if (this.siteSettings.allow_uncategorized_topics || this.get("allowUncategorized")) { - if (!isNone(rootNone)) { - return rootNoneLabel || "category.none"; - } else { - return Category.findUncategorized(); - } - } else { - return "category.choose"; - } - }, - - @computed - templateForRow() { - return rowComponent => this._rowContentTemplate(rowComponent.get("content")); - }, - - @computed - templateForNoneRow() { - return rowComponent => this._rowContentTemplate(rowComponent.get("content")); - }, - - @computed("scopedCategoryId", "content.[]") - computedContent(scopedCategoryId, categories) { - // Always scope to the parent of a category, if present - if (scopedCategoryId) { - const scopedCat = Category.findById(scopedCategoryId); - scopedCategoryId = scopedCat.get("parent_category_id") || scopedCat.get("id"); - } - - const excludeCategoryId = this.get("excludeCategoryId"); - - return categories.filter(c => { - const categoryId = get(c, "value"); - if (scopedCategoryId && categoryId !== scopedCategoryId && get(c, "originalContent.parent_category_id") !== scopedCategoryId) { - return false; - } - if (get(c, 'originalContent.isUncategorizedCategory') || excludeCategoryId === categoryId) { - return false; - } - return get(c, 'originalContent.permission') === PermissionType.FULL; - }); - }, - - @on("didRender") - _bindComposerResizing() { - this.appEvents.on("composer:resized", this, this.applyDirection); - }, - - @on("willDestroyElement") - _unbindComposerResizing() { - this.appEvents.off("composer:resized"); - }, - - @computed("site.sortedCategories") - content() { - const categories = Discourse.SiteSettings.fixed_category_positions_on_create ? - Category.list() : - Category.listByActivity(); - return this.formatContents(categories); - }, - - _rowContentTemplate(content) { - let category; - - // If we have no id, but text with the uncategorized name, we can use that badge. - if (isEmpty(get(content, "value"))) { - const uncat = Category.findUncategorized(); - if (uncat && uncat.get("name") === get(content, "name")) { - category = uncat; - } - } else { - category = Category.findById(parseInt(get(content, "value"), 10)); - } - - if (!category) return get(content, "name"); - let result = categoryBadgeHTML(category, {link: false, allowUncategorized: true, hideParent: true}); - const parentCategoryId = category.get("parent_category_id"); - - if (parentCategoryId) { - result = `
    ${categoryBadgeHTML(Category.findById(parentCategoryId), {link: false})} ${result}`; - } else { - result = `
    ${result}`; - } - - result += ` × ${category.get("topic_count")}
    `; - - const description = category.get("description"); - // TODO wtf how can this be null?; - if (description && description !== "null") { - result += `
    ${description.substr(0, 200)}${description.length > 200 ? '…' : ''}
    `; - } - - return result; - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/category-notifications-button.js.es6 b/app/assets/javascripts/select-box-kit/components/category-notifications-button.js.es6 deleted file mode 100644 index d022c3c64c..0000000000 --- a/app/assets/javascripts/select-box-kit/components/category-notifications-button.js.es6 +++ /dev/null @@ -1,17 +0,0 @@ -import NotificationOptionsComponent from "select-box-kit/components/notifications-button"; - -export default NotificationOptionsComponent.extend({ - classNames: "category-notifications-button", - isHidden: Ember.computed.or("category.deleted", "site.isMobileDevice"), - i18nPrefix: "category.notifications", - value: Ember.computed.alias("category.notification_level"), - headerComponent: "category-notifications-button/category-notifications-button-header", - - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.get("category").setNotification(value); - this.blur(); - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/category-notifications-button/category-notifications-button-header.js.es6 b/app/assets/javascripts/select-box-kit/components/category-notifications-button/category-notifications-button-header.js.es6 deleted file mode 100644 index 8c5ef82fe3..0000000000 --- a/app/assets/javascripts/select-box-kit/components/category-notifications-button/category-notifications-button-header.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import NotificationButtonHeader from "select-box-kit/components/notifications-button/notifications-button-header"; -import computed from "ember-addons/ember-computed-decorators"; -import { iconHTML } from 'discourse-common/lib/icon-library'; - -export default NotificationButtonHeader.extend({ - classNames: "category-notifications-button-header", - shouldDisplaySelectedName: false, - - @computed("_selectedDetails.icon", "_selectedDetails.key") - icon() { - return `${this._super()}${iconHTML("caret-down")}`.htmlSafe(); - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/combo-box/combo-box-header.js.es6 b/app/assets/javascripts/select-box-kit/components/combo-box/combo-box-header.js.es6 deleted file mode 100644 index 996ca1f472..0000000000 --- a/app/assets/javascripts/select-box-kit/components/combo-box/combo-box-header.js.es6 +++ /dev/null @@ -1,39 +0,0 @@ -import SelectBoxKitHeaderComponent from "select-box-kit/components/select-box-kit/select-box-kit-header"; -import { default as computed } from "ember-addons/ember-computed-decorators"; - -export default SelectBoxKitHeaderComponent.extend({ - layoutName: "select-box-kit/templates/components/combo-box/combo-box-header", - classNames: "combo-box-header", - - clearable: Ember.computed.alias("options.clearable"), - caretUpIcon: Ember.computed.alias("options.caretUpIcon"), - caretDownIcon: Ember.computed.alias("options.caretDownIcon"), - selectedName: Ember.computed.alias("options.selectedName"), - - @computed("isExpanded", "caretUpIcon", "caretDownIcon") - caretIcon(isExpanded, caretUpIcon, caretDownIcon) { - return isExpanded === true ? caretUpIcon : caretDownIcon; - }, - - @computed("clearable", "selectedContent") - shouldDisplayClearableButton(clearable, selectedContent) { - return clearable === true && !Ember.isEmpty(selectedContent); - }, - - @computed("options.selectedName", "selectedContent.firstObject.name", "none.name") - selectedName(selectedName, name, noneName) { - if (Ember.isPresent(selectedName)) { - return selectedName; - } - - if (Ember.isNone(name)) { - if (Ember.isNone(noneName)) { - return this._super(); - } else { - return noneName; - } - } else { - return name; - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 b/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 deleted file mode 100644 index 8e2789aa1d..0000000000 --- a/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -import SelectBoxKitComponent from "select-box-kit/components/select-box-kit"; - -export default SelectBoxKitComponent.extend({ - classNames: "dropdown-select-box", - verticalOffset: 3, - fullWidthOnMobile: true, - filterable: false, - autoFilterable: false, - headerComponent: "dropdown-select-box/dropdown-select-box-header", - rowComponent: "dropdown-select-box/dropdown-select-box-row", - - clickOutside() { - this.close(); - }, - - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.set("value", value); - - this.blur(); - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 b/app/assets/javascripts/select-box-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 deleted file mode 100644 index 53dc082bdc..0000000000 --- a/app/assets/javascripts/select-box-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 +++ /dev/null @@ -1,6 +0,0 @@ -import SelectBoxKitHeaderComponent from "select-box-kit/components/select-box-kit/select-box-kit-header"; - -export default SelectBoxKitHeaderComponent.extend({ - layoutName: "select-box-kit/templates/components/dropdown-select-box/dropdown-select-box-header", - classNames: "dropdown-select-box-header", -}); diff --git a/app/assets/javascripts/select-box-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 b/app/assets/javascripts/select-box-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 deleted file mode 100644 index 38ce2a7aa0..0000000000 --- a/app/assets/javascripts/select-box-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 +++ /dev/null @@ -1,9 +0,0 @@ -import SelectBoxKitRowComponent from "select-box-kit/components/select-box-kit/select-box-kit-row"; - -export default SelectBoxKitRowComponent.extend({ - layoutName: "select-box-kit/templates/components/dropdown-select-box/dropdown-select-box-row", - classNames: "dropdown-select-box-row", - - name: Ember.computed.alias("content.name"), - description: Ember.computed.alias("content.originalContent.description") -}); diff --git a/app/assets/javascripts/select-box-kit/components/future-date-input-selector/future-date-input-selector-header.js.es6 b/app/assets/javascripts/select-box-kit/components/future-date-input-selector/future-date-input-selector-header.js.es6 deleted file mode 100644 index e62e127625..0000000000 --- a/app/assets/javascripts/select-box-kit/components/future-date-input-selector/future-date-input-selector-header.js.es6 +++ /dev/null @@ -1,14 +0,0 @@ -import ComboBoxHeaderComponent from "select-box-kit/components/combo-box/combo-box-header"; -import DatetimeMixin from "select-box-kit/components/future-date-input-selector/mixin"; -import computed from "ember-addons/ember-computed-decorators"; - -export default ComboBoxHeaderComponent.extend(DatetimeMixin, { - layoutName: "select-box-kit/templates/components/future-date-input-selector/future-date-input-selector-header", - classNames: "future-date-input-selector-header", - - @computed("selectedContent.firstObject.value") - datetime(value) { return this._computeDatetimeForValue(value); }, - - @computed("selectedContent.firstObject.value") - icon(value) { return this._computeIconForValue(value); } -}); diff --git a/app/assets/javascripts/select-box-kit/components/future-date-input-selector/future-date-input-selector-row.js.es6 b/app/assets/javascripts/select-box-kit/components/future-date-input-selector/future-date-input-selector-row.js.es6 deleted file mode 100644 index be35212a76..0000000000 --- a/app/assets/javascripts/select-box-kit/components/future-date-input-selector/future-date-input-selector-row.js.es6 +++ /dev/null @@ -1,14 +0,0 @@ -import SelectBoxKitRowComponent from "select-box-kit/components/select-box-kit/select-box-kit-row"; -import DatetimeMixin from "select-box-kit/components/future-date-input-selector/mixin"; -import computed from "ember-addons/ember-computed-decorators"; - -export default SelectBoxKitRowComponent.extend(DatetimeMixin, { - layoutName: "select-box-kit/templates/components/future-date-input-selector/future-date-input-selector-row", - classNames: "future-date-input-selector-row", - - @computed("content.value") - datetime(value) { return this._computeDatetimeForValue(value); }, - - @computed("content.value") - icon(value) { return this._computeIconForValue(value); } -}); diff --git a/app/assets/javascripts/select-box-kit/components/group-notifications-button.js.es6 b/app/assets/javascripts/select-box-kit/components/group-notifications-button.js.es6 deleted file mode 100644 index 81477d2e47..0000000000 --- a/app/assets/javascripts/select-box-kit/components/group-notifications-button.js.es6 +++ /dev/null @@ -1,14 +0,0 @@ -import NotificationOptionsComponent from "select-box-kit/components/notifications-button"; - -export default NotificationOptionsComponent.extend({ - classNames: ["group-notifications-button"], - value: Ember.computed.alias("group.group_user.notification_level"), - i18nPrefix: "groups.notifications", - - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.get("group").setNotification(value, this.get("user.id")); - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/multi-combo-box.js.es6 b/app/assets/javascripts/select-box-kit/components/multi-combo-box.js.es6 deleted file mode 100644 index 6ce70b4e8e..0000000000 --- a/app/assets/javascripts/select-box-kit/components/multi-combo-box.js.es6 +++ /dev/null @@ -1,99 +0,0 @@ -// Experimental -import SelectBoxKitComponent from "select-box-kit/components/select-box-kit"; -import computed from "ember-addons/ember-computed-decorators"; -const { get, isNone } = Ember; - -export default SelectBoxKitComponent.extend({ - classNames: "multi-combobox", - headerComponent: "multi-combo-box/multi-combo-box-header", - filterComponent: null, - headerText: "select_box.default_header_text", - value: [], - allowAny: true, - - @computed("filter") - templateForCreateRow() { - return (rowComponent) => { - return `Create: ${rowComponent.get("content.name")}`; - }; - }, - - keyDown(event) { - const keyCode = event.keyCode || event.which; - const $filterInput = this.$filterInput(); - - if (keyCode === 8) { - let $lastSelectedValue = $(this.$(".choices .selected-name").last()); - - if ($lastSelectedValue.is(":focus") || $(document.activeElement).is($lastSelectedValue)) { - this.send("onDeselect", $lastSelectedValue.data("value")); - $filterInput.focus(); - return; - } - - if ($filterInput.val() === "") { - if ($filterInput.is(":focus")) { - if ($lastSelectedValue.length > 0) { - $lastSelectedValue.focus(); - } - } else { - if ($lastSelectedValue.length > 0) { - $lastSelectedValue.focus(); - } else { - $filterInput.focus(); - } - } - } - } else { - $filterInput.focus(); - this._super(event); - } - }, - - @computed("none") - computedNone(none) { - if (!isNone(none)) { - this.set("none", { name: I18n.t(none), value: "" }); - } - }, - - @computed("value.[]") - computedValue(value) { - return value.map(v => this._castInteger(v)); - }, - - @computed("computedValue.[]", "computedContent.[]") - selectedContent(computedValue, computedContent) { - const contents = []; - computedValue.forEach(cv => { - contents.push(computedContent.findBy("value", cv)); - }); - return contents; - }, - - filterFunction(content) { - return (selectBox, computedValue) => { - const filter = selectBox.get("filter").toLowerCase(); - return _.filter(content, c => { - return !computedValue.includes(get(c, "value")) && - get(c, "name").toLowerCase().indexOf(filter) > -1; - }); - }; - }, - - actions: { - onClearSelection() { - this.send("onSelect", []); - }, - - onSelect(value) { - this.setProperties({ filter: "", highlightedValue: null }); - this.get("value").pushObject(value); - }, - - onDeselect(value) { - this.defaultOnDeselect(value); - this.get("value").removeObject(value); - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/multi-combo-box/multi-combo-box-header.js.es6 b/app/assets/javascripts/select-box-kit/components/multi-combo-box/multi-combo-box-header.js.es6 deleted file mode 100644 index 0b5f302e7b..0000000000 --- a/app/assets/javascripts/select-box-kit/components/multi-combo-box/multi-combo-box-header.js.es6 +++ /dev/null @@ -1,40 +0,0 @@ -import { on } from "ember-addons/ember-computed-decorators"; -import computed from "ember-addons/ember-computed-decorators"; -import SelectBoxKitHeaderComponent from "select-box-kit/components/select-box-kit/select-box-kit-header"; - -export default SelectBoxKitHeaderComponent.extend({ - attributeBindings: ["names:data-name"], - classNames: "multi-combobox-header", - layoutName: "select-box-kit/templates/components/multi-combo-box/multi-combo-box-header", - - @computed("filter", "selectedContent.[]", "isFocused", "selectBoxIsExpanded") - shouldDisplayFilterPlaceholder(filter, selectedContent, isFocused) { - if (Ember.isEmpty(selectedContent)) { - if (filter.length > 0) { return false; } - if (isFocused === true) { return false; } - return true; - } - - return false; - }, - - @on("didRender") - _positionFilter() { - this.$(".filter").width(0); - - const leftHeaderOffset = this.$().offset().left; - const leftFilterOffset = this.$(".filter").offset().left; - const offset = leftFilterOffset - leftHeaderOffset; - const width = this.$().outerWidth(false); - const availableSpace = width - offset; - - // TODO: avoid magic number 8 - // TODO: make sure the filter doesn’t end up being very small - this.$(".filter").width(availableSpace - 8); - }, - - @computed("selectedContent.[]") - names(selectedContent) { - return selectedContent.map(sc => sc.name).join(","); - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-header.js.es6 b/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-header.js.es6 deleted file mode 100644 index 96b1553492..0000000000 --- a/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-header.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -import DropdownSelectBoxHeaderComponent from "select-box-kit/components/dropdown-select-box/dropdown-select-box-header"; -import computed from "ember-addons/ember-computed-decorators"; -import { iconHTML } from 'discourse-common/lib/icon-library'; -import { buttonDetails } from "discourse/lib/notification-levels"; - -export default DropdownSelectBoxHeaderComponent.extend({ - classNames: "notifications-button-header", - - i18nPrefix: Ember.computed.alias("options.i18nPrefix"), - shouldDisplaySelectedName: Ember.computed.alias("options.showFullTitle"), - - @computed("_selectedDetails.icon", "_selectedDetails.key") - icon(icon, key) { - return iconHTML(icon, {class: key}).htmlSafe(); - }, - - @computed("_selectedDetails.key", "i18nPrefix") - selectedName(key, prefix) { - return I18n.t(`${prefix}.${key}.title`); - }, - - @computed("selectedContent.firstObject.value") - _selectedDetails(value) { return buttonDetails(value); } -}); diff --git a/app/assets/javascripts/select-box-kit/components/pinned-options.js.es6 b/app/assets/javascripts/select-box-kit/components/pinned-options.js.es6 deleted file mode 100644 index c1be2e6ab3..0000000000 --- a/app/assets/javascripts/select-box-kit/components/pinned-options.js.es6 +++ /dev/null @@ -1,64 +0,0 @@ -import DropdownSelectBoxComponent from "select-box-kit/components/dropdown-select-box"; -import computed from "ember-addons/ember-computed-decorators"; -import { observes } from "ember-addons/ember-computed-decorators"; -import { on } from "ember-addons/ember-computed-decorators"; - -export default DropdownSelectBoxComponent.extend({ - classNames: "pinned-options", - - headerComponent: "pinned-options/pinned-options-header", - - @on("didReceiveAttrs") - _setComponentOptions() { - this.set("headerComponentOptions", Ember.Object.create({ - pinned: this.get("topic.pinned"), - pinnedGlobally: this.get("topic.pinned_globally") - })); - }, - - @computed("topic.pinned") - value(pinned) { - return pinned ? "pinned" : "unpinned"; - }, - - @observes("topic.pinned") - _pinStateChanged() { - this.set("value", this.get("topic.pinned") ? "pinned" : "unpinned"); - this._setComponentOptions(); - }, - - @computed("topic.pinned_globally") - content(pinnedGlobally) { - const globally = pinnedGlobally ? "_globally" : ""; - - return [ - { - id: "pinned", - name: I18n.t("topic_statuses.pinned" + globally + ".title"), - description: I18n.t('topic_statuses.pinned' + globally + '.help'), - icon: "thumb-tack" - }, - { - id: "unpinned", - name: I18n.t("topic_statuses.unpinned.title"), - icon: "thumb-tack", - description: I18n.t('topic_statuses.unpinned.help'), - iconClass: "unpinned" - } - ]; - }, - - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - - const topic = this.get("topic"); - - if (value === "unpinned") { - topic.clearPin(); - } else { - topic.rePin(); - } - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/pinned-options/pinned-options-header.js.es6 b/app/assets/javascripts/select-box-kit/components/pinned-options/pinned-options-header.js.es6 deleted file mode 100644 index 3f83e8bffd..0000000000 --- a/app/assets/javascripts/select-box-kit/components/pinned-options/pinned-options-header.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -import DropdownSelectBoxHeaderComponent from "select-box-kit/components/dropdown-select-box/dropdown-select-box-header"; -import computed from "ember-addons/ember-computed-decorators"; -import { iconHTML } from 'discourse-common/lib/icon-library'; - -export default DropdownSelectBoxHeaderComponent.extend({ - classNames: "pinned-options-header", - - pinnedGlobally: Ember.computed.alias("options.pinnedGlobally"), - pinned: Ember.computed.alias("options.pinned"), - - @computed("pinned", "pinnedGlobally") - icon(pinned, pinnedGlobally) { - const globally = pinnedGlobally ? "_globally" : ""; - const state = pinned ? `pinned${globally}` : "unpinned"; - - return iconHTML( - "thumb-tack", - { class: (state === "unpinned" ? "unpinned" : null) } - ); - }, - - @computed("pinned", "pinnedGlobally") - selectedName(pinned, pinnedGlobally) { - const globally = pinnedGlobally ? "_globally" : ""; - const state = pinned ? `pinned${globally}` : "unpinned"; - const title = I18n.t(`topic_statuses.${state}.title`); - - return `${title}${iconHTML("caret-down")}`.htmlSafe(); - }, -}); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 deleted file mode 100644 index 486b39bd70..0000000000 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 +++ /dev/null @@ -1,494 +0,0 @@ -const { get, isNone, isEmpty, isPresent } = Ember; -import { on, observes } from "ember-addons/ember-computed-decorators"; -import computed from "ember-addons/ember-computed-decorators"; -import UtilsMixin from "select-box-kit/mixins/utils"; -import DomHelpersMixin from "select-box-kit/mixins/dom-helpers"; -import KeyboardMixin from "select-box-kit/mixins/keyboard"; - -export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin, { - layoutName: "select-box-kit/templates/components/select-box-kit", - classNames: "select-box-kit", - classNameBindings: [ - "isFocused", - "isExpanded", - "isDisabled", - "isHidden", - "isAbove", - "isBelow", - "isLeftAligned", - "isRightAligned" - ], - isDisabled: false, - isExpanded: false, - isFocused: false, - isHidden: false, - renderBody: false, - tabindex: 0, - scrollableParentSelector: ".modal-body", - value: null, - none: null, - highlightedValue: null, - noContentLabel: "select_box.no_content", - valueAttribute: "id", - nameProperty: "name", - autoFilterable: false, - filterable: false, - filter: "", - filterPlaceholder: "select_box.filter_placeholder", - filterIcon: "search", - rowComponent: "select-box-kit/select-box-kit-row", - noneRowComponent: "select-box-kit/select-box-kit-none-row", - createRowComponent: "select-box-kit/select-box-kit-create-row", - filterComponent: "select-box-kit/select-box-kit-filter", - headerComponent: "select-box-kit/select-box-kit-header", - collectionComponent: "select-box-kit/select-box-kit-collection", - collectionHeight: 200, - verticalOffset: 0, - horizontalOffset: 0, - fullWidthOnMobile: false, - castInteger: false, - allowAny: false, - allowValueMutation: true, - autoSelectFirst: true, - - init() { - this._super(); - - if ($(window).outerWidth(false) <= 420) { - this.setProperties({ filterable: false, autoFilterable: false }); - } - - this._previousScrollParentOverflow = "auto"; - this._previousCSSContext = {}; - }, - - click(event) { - event.stopPropagation(); - }, - - close() { - this.setProperties({ isExpanded: false, isFocused: false }); - }, - - focus() { - Ember.run.schedule("afterRender", () => this.$offscreenInput().select() ); - }, - - blur() { - Ember.run.schedule("afterRender", () => this.$offscreenInput().blur() ); - }, - - clickOutside(event) { - if ($(event.target).parents(".select-box-kit").length === 1) { - this.close(); - return; - } - - if (this.get("isExpanded") === true) { - this.set("isExpanded", false); - this.focus(); - } else { - this.close(); - } - }, - - createFunction(input) { - return (selectedBox) => { - const formatedContent = selectedBox.formatContent(input); - formatedContent.meta.generated = true; - return formatedContent; - }; - }, - - filterFunction(content) { - return selectBox => { - const filter = selectBox.get("filter").toLowerCase(); - return _.filter(content, c => { - return get(c, "name").toLowerCase().indexOf(filter) > -1; - }); - }; - }, - - nameForContent(content) { - if (isNone(content)) { - return null; - } - - if (typeof content === "object") { - return get(content, this.get("nameProperty")); - } - - return content; - }, - - valueForContent(content) { - switch (typeof content) { - case "string": - case "number": - return this._castInteger(content); - default: - return this._castInteger(get(content, this.get("valueAttribute"))); - } - }, - - formatContent(content) { - return { - value: this.valueForContent(content), - name: this.nameForContent(content), - originalContent: content, - meta: { generated: false } - }; - }, - - formatContents(contents) { - return contents.map(content => this.formatContent(content)); - }, - - @computed("filter", "filterable", "autoFilterable") - computedFilterable(filter, filterable, autoFilterable) { - if (filterable === true) { - return true; - } - - if (filter.length > 0 && autoFilterable === true) { - return true; - } - - return false; - }, - - @computed("computedFilterable", "filter", "allowAny") - shouldDisplayCreateRow(computedFilterable, filter, allow) { - return computedFilterable === true && filter.length > 0 && allow === true; - }, - - @computed("filter", "allowAny") - createRowContent(filter, allow) { - if (allow === true) { - return Ember.Object.create({ value: filter, name: filter }); - } - }, - - @computed("content.[]") - computedContent(content) { - this._mutateValue(); - return this.formatContents(content || []); - }, - - @computed("value", "none", "computedContent.firstObject.value") - computedValue(value, none, firstContentValue) { - if (isNone(value) && isNone(none) && this.get("autoSelectFirst") === true) { - return this._castInteger(firstContentValue); - } - - return this._castInteger(value); - }, - - @computed - templateForRow() { return () => null; }, - - @computed - templateForNoneRow() { return () => null; }, - - @computed - templateForCreateRow() { return () => null; }, - - @computed("none") - computedNone(none) { - if (isNone(none)) { - return null; - } - - switch (typeof none) { - case "string": - return Ember.Object.create({ name: I18n.t(none), value: "" }); - default: - return this.formatContent(none); - } - }, - - @computed("computedValue", "computedContent.[]") - selectedContent(computedValue, computedContent) { - if (isNone(computedValue)) { - return []; - } - - return [ computedContent.findBy("value", this._castInteger(computedValue)) ]; - }, - - @on("didRender") - _configureSelectBoxDOM() { - if (this.get("isExpanded") === true) { - Ember.run.schedule("afterRender", () => { - this.$collection().css("max-height", this.get("collectionHeight")); - this._applyDirection(); - this._positionWrapper(); - }); - } - }, - - @on("willDestroyElement") - _cleanHandlers() { - $(window).off("resize.select-box-kit"); - this._removeFixedPosition(); - }, - - @on("didInsertElement") - _setupResizeListener() { - $(window).on("resize.select-box-kit", () => this.set("isExpanded", false) ); - }, - - @observes("filter", "filteredContent.[]", "shouldDisplayCreateRow") - _setHighlightedValue() { - const filteredContent = this.get("filteredContent"); - const display = this.get("shouldDisplayCreateRow"); - const none = this.get("computedNone"); - - if (isNone(this.get("highlightedValue")) && !isEmpty(filteredContent)) { - this.set("highlightedValue", get(filteredContent, "firstObject.value")); - return; - } - - if (display === true && isEmpty(filteredContent)) { - this.set("highlightedValue", this.get("filter")); - } - else if (!isEmpty(filteredContent)) { - this.set("highlightedValue", get(filteredContent, "firstObject.value")); - } - else if (isEmpty(filteredContent) && isPresent(none) && display === false) { - this.set("highlightedValue", get(none, "value")); - } - }, - - @observes("isExpanded") - _isExpandedChanged() { - if (this.get("isExpanded") === true) { - this._applyFixedPosition(); - - this.setProperties({ - highlightedValue: this.get("computedValue"), - renderBody: true, - isFocused: true - }); - } else { - this._removeFixedPosition(); - } - }, - - @computed("filter", "computedFilterable", "computedContent.[]", "computedValue.[]") - filteredContent(filter, computedFilterable, computedContent, computedValue) { - if (computedFilterable === false) { return computedContent; } - return this.filterFunction(computedContent)(this, computedValue); - }, - - @computed("scrollableParentSelector") - scrollableParent(scrollableParentSelector) { - return this.$().parents(scrollableParentSelector).first(); - }, - - actions: { - onToggle() { - this.toggleProperty("isExpanded"); - - if (this.get("isExpanded") === true) { this.focus(); } - }, - - onCreateContent(input) { - const content = this.createFunction(input)(this); - this.get("computedContent").pushObject(content); - this.send("onSelect", content.value); - }, - - onFilterChange(filter) { - this.set("filter", filter); - }, - - onHighlight(value) { - this.set("highlightedValue", value); - }, - - onClearSelection() { - this.send("onSelect", null); - }, - - onSelect(value) { - value = this.defaultOnSelect(value); - this.set("value", value); - }, - - onDeselect() { - this.defaultOnDeselect(); - this.set("value", null); - } - }, - - defaultOnSelect(value) { - if (value === "") { value = null; } - - this.setProperties({ - highlightedValue: null, - isExpanded: false, - filter: "" - }); - - this.focus(); - - return value; - }, - - defaultOnDeselect(value) { - const content = this.get("computedContent").findBy("value", value); - if (!isNone(content) && get(content, "meta.generated") === true) { - this.get("computedContent").removeObject(content); - } - }, - - _applyDirection() { - let options = { left: "auto", bottom: "auto", top: "auto" }; - - const dHeader = $(".d-header")[0]; - const dHeaderBounds = dHeader ? dHeader.getBoundingClientRect() : {top: 0, height: 0}; - const dHeaderHeight = dHeaderBounds.top + dHeaderBounds.height; - const headerHeight = this.$header().outerHeight(false); - const headerWidth = this.$header().outerWidth(false); - const bodyHeight = this.$body().outerHeight(false); - const windowWidth = $(window).width(); - const windowHeight = $(window).height(); - const boundingRect = this.get("element").getBoundingClientRect(); - const offsetTop = boundingRect.top; - const offsetBottom = boundingRect.bottom; - - if (this.get("fullWidthOnMobile") && windowWidth <= 420) { - const margin = 10; - const relativeLeft = this.$().offset().left - $(window).scrollLeft(); - options.left = margin - relativeLeft; - options.width = windowWidth - margin * 2; - options.maxWidth = options.minWidth = "unset"; - } else { - const bodyWidth = this.$body().outerWidth(false); - - if ($("html").css("direction") === "rtl") { - const horizontalSpacing = boundingRect.right; - const hasHorizontalSpace = horizontalSpacing - (this.get("horizontalOffset") + bodyWidth) > 0; - if (hasHorizontalSpace) { - this.setProperties({ isLeftAligned: true, isRightAligned: false }); - options.left = bodyWidth + this.get("horizontalOffset"); - } else { - this.setProperties({ isLeftAligned: false, isRightAligned: true }); - options.right = - (bodyWidth - headerWidth + this.get("horizontalOffset")); - } - } else { - const horizontalSpacing = boundingRect.left; - const hasHorizontalSpace = (windowWidth - (this.get("horizontalOffset") + horizontalSpacing + bodyWidth) > 0); - if (hasHorizontalSpace) { - this.setProperties({ isLeftAligned: true, isRightAligned: false }); - options.left = this.get("horizontalOffset"); - } else { - this.setProperties({ isLeftAligned: false, isRightAligned: true }); - options.right = this.get("horizontalOffset"); - } - } - } - - const componentHeight = this.get("verticalOffset") + bodyHeight + headerHeight; - const hasBelowSpace = windowHeight - offsetBottom - componentHeight > 0; - const hasAboveSpace = offsetTop - componentHeight - dHeaderHeight > 0; - if (hasBelowSpace || (!hasBelowSpace && !hasAboveSpace)) { - this.setProperties({ isBelow: true, isAbove: false }); - options.top = headerHeight + this.get("verticalOffset"); - } else { - this.setProperties({ isBelow: false, isAbove: true }); - options.bottom = headerHeight + this.get("verticalOffset"); - } - - this.$body().css(options); - }, - - _applyFixedPosition() { - const width = this.$().outerWidth(false); - const height = this.$header().outerHeight(false); - - if (this.get("scrollableParent").length === 0) { return; } - - const $placeholder = $(`
    `); - - this._previousScrollParentOverflow = this.get("scrollableParent").css("overflow"); - this.get("scrollableParent").css({ overflow: "hidden" }); - - this._previousCSSContext = { - minWidth: this.$().css("min-width"), - maxWidth: this.$().css("max-width") - }; - - const componentStyles = { - position: "fixed", - "margin-top": -this.get("scrollableParent").scrollTop(), - width, - minWidth: "unset", - maxWidth: "unset" - }; - - if ($("html").css("direction") === "rtl") { - componentStyles.marginRight = -width; - } else { - componentStyles.marginLeft = -width; - } - - $placeholder.css({ display: "inline-block", width, height, "vertical-align": "middle" }); - - this.$().before($placeholder).css(componentStyles); - }, - - _removeFixedPosition() { - if (this.get("scrollableParent").length === 0) { - return; - } - - $(`.select-box-kit-fixed-placeholder-${this.elementId}`).remove(); - - const css = _.extend( - this._previousCSSContext, - { - top: "auto", - left: "auto", - "margin-left": "auto", - "margin-right": "auto", - "margin-top": "auto", - position: "relative" - } - ); - this.$().css(css); - - this.get("scrollableParent").css({ - overflow: this._previousScrollParentOverflow - }); - }, - - _positionWrapper() { - const headerHeight = this.$header().outerHeight(false); - - this.$(".select-box-kit-wrapper").css({ - width: this.$().width(), - height: headerHeight + this.$body().outerHeight(false) - }); - }, - - @on("didReceiveAttrs") - _mutateValue() { - if (this.get("allowValueMutation") !== true) { - return; - } - - const none = isNone(this.get("none")); - const emptyValue = isEmpty(this.get("value")); - - if (none && emptyValue) { - Ember.run.scheduleOnce("sync", () => { - if (!isEmpty(this.get("computedContent"))) { - const firstValue = this.get("computedContent.firstObject.value"); - this.set("value", firstValue); - } - }); - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-collection.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-collection.js.es6 deleted file mode 100644 index ca378f5ef9..0000000000 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-collection.js.es6 +++ /dev/null @@ -1,5 +0,0 @@ -export default Ember.Component.extend({ - layoutName: "select-box-kit/templates/components/select-box-kit/select-box-kit-collection", - classNames: "select-box-kit-collection", - tagName: "ul" -}); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-create-row.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-create-row.js.es6 deleted file mode 100644 index fd686080ec..0000000000 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-create-row.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -import SelectBoxKitRowComponent from "select-box-kit/components/select-box-kit/select-box-kit-row"; - -export default SelectBoxKitRowComponent.extend({ - layoutName: "select-box-kit/templates/components/select-box-kit/select-box-kit-row", - classNames: "create", - - click() { - this.sendAction("onCreateContent", this.get("content.name")); - }, -}); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-filter.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-filter.js.es6 deleted file mode 100644 index 9717b59f78..0000000000 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-filter.js.es6 +++ /dev/null @@ -1,6 +0,0 @@ -export default Ember.Component.extend({ - layoutName: "select-box-kit/templates/components/select-box-kit/select-box-kit-filter", - classNames: "select-box-kit-filter", - classNameBindings: ["isFocused", "isHidden"], - isHidden: Ember.computed.not("filterable"), -}); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-header.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-header.js.es6 deleted file mode 100644 index b7797571ae..0000000000 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-header.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -import computed from 'ember-addons/ember-computed-decorators'; - -export default Ember.Component.extend({ - layoutName: "select-box-kit/templates/components/select-box-kit/select-box-kit-header", - classNames: "select-box-kit-header", - classNameBindings: ["isFocused"], - attributeBindings: ["selectedName:data-name"], - shouldDisplaySelectedName: true, - - @computed("options.shouldDisplaySelectedName") - shouldDisplaySelectedName(should) { - if (Ember.isNone(should)) { return true; } - return should; - }, - - @computed("options.selectedName", "selectedContent.firstObject.name") - selectedName(optionsSelectedName, firstSelectedContentName) { - if (Ember.isNone(optionsSelectedName)) { - return firstSelectedContentName; - } - return optionsSelectedName; - }, - - @computed("options.icon") - icon(optionsIcon) { return optionsIcon; }, - - click() { this.sendAction("onToggle"); } -}); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-none-row.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-none-row.js.es6 deleted file mode 100644 index fbc07b312c..0000000000 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-none-row.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -import SelectBoxKitRowComponent from "select-box-kit/components/select-box-kit/select-box-kit-row"; - -export default SelectBoxKitRowComponent.extend({ - layoutName: "select-box-kit/templates/components/select-box-kit/select-box-kit-row", - classNames: "none", - - click() { - this.sendAction("onClearSelection"); - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 deleted file mode 100644 index efdb299a56..0000000000 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 +++ /dev/null @@ -1,59 +0,0 @@ -import { iconHTML } from 'discourse-common/lib/icon-library'; -import { on } from 'ember-addons/ember-computed-decorators'; -import computed from 'ember-addons/ember-computed-decorators'; -const { run, isPresent } = Ember; -import UtilsMixin from "select-box-kit/mixins/utils"; - -export default Ember.Component.extend(UtilsMixin, { - layoutName: "select-box-kit/templates/components/select-box-kit/select-box-kit-row", - classNames: "select-box-kit-row", - tagName: "li", - attributeBindings: [ - "title", - "content.value:data-value", - "content.name:data-name" - ], - classNameBindings: ["isHighlighted", "isSelected"], - - title: Ember.computed.alias("content.name"), - - @computed("templateForRow") - template(templateForRow) { return templateForRow(this); }, - - @on("didReceiveAttrs") - _setSelectionState() { - const contentValue = this.get("content.value"); - this.set("isSelected", this.get("value") === contentValue); - this.set("isHighlighted", this._castInteger(this.get("highlightedValue")) === this._castInteger(contentValue)); - }, - - @on("willDestroyElement") - _clearDebounce() { - const hoverDebounce = this.get("hoverDebounce"); - - if (isPresent(hoverDebounce)) { - run.cancel(hoverDebounce); - } - }, - - @computed("content.originalContent.icon", "content.originalContent.iconClass") - icon(icon, cssClass) { - if (icon) { - return iconHTML(icon, { class: cssClass }); - } - - return null; - }, - - mouseEnter() { - this.set("hoverDebounce", run.debounce(this, this._sendOnHighlightAction, 32)); - }, - - click() { - this.sendAction("onSelect", this.get("content.value")); - }, - - _sendOnHighlightAction() { - this.sendAction("onHighlight", this.get("content.value")); - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/tag-notifications-button.js.es6 b/app/assets/javascripts/select-box-kit/components/tag-notifications-button.js.es6 deleted file mode 100644 index 2ab3ed375b..0000000000 --- a/app/assets/javascripts/select-box-kit/components/tag-notifications-button.js.es6 +++ /dev/null @@ -1,16 +0,0 @@ -import NotificationOptionsComponent from "select-box-kit/components/notifications-button"; - -export default NotificationOptionsComponent.extend({ - classNames: "tag-notifications-button", - i18nPrefix: "tagging.notifications", - showFullTitle: false, - headerComponent: "tag-notifications-button/tag-notifications-button-header", - - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.sendAction("action", value); - this.blur(); - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/tag-notifications-button/tag-notifications-button-header.js.es6 b/app/assets/javascripts/select-box-kit/components/tag-notifications-button/tag-notifications-button-header.js.es6 deleted file mode 100644 index 524c8b8585..0000000000 --- a/app/assets/javascripts/select-box-kit/components/tag-notifications-button/tag-notifications-button-header.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import NotificationButtonHeader from "select-box-kit/components/notifications-button/notifications-button-header"; -import computed from "ember-addons/ember-computed-decorators"; -import { iconHTML } from 'discourse-common/lib/icon-library'; - -export default NotificationButtonHeader.extend({ - classNames: "tag-notifications-button-header", - shouldDisplaySelectedName: false, - - @computed("_selectedDetails.icon", "_selectedDetails.key") - icon() { - return `${this._super()}${iconHTML("caret-down")}`.htmlSafe(); - } -}); diff --git a/app/assets/javascripts/select-box-kit/components/topic-footer-mobile-dropdown.js.es6 b/app/assets/javascripts/select-box-kit/components/topic-footer-mobile-dropdown.js.es6 deleted file mode 100644 index 2fb8f3e317..0000000000 --- a/app/assets/javascripts/select-box-kit/components/topic-footer-mobile-dropdown.js.es6 +++ /dev/null @@ -1,76 +0,0 @@ -import computed from "ember-addons/ember-computed-decorators"; -import ComboBoxComponent from "select-box-kit/components/combo-box"; -import { on } from "ember-addons/ember-computed-decorators"; - -export default ComboBoxComponent.extend({ - headerText: "topic.controls", - classNames: "topic-footer-mobile-dropdown", - filterable: false, - autoFilterable: false, - allowValueMutation: false, - autoSelectFirst: false, - - @on("didReceiveAttrs") - _setTopicFooterMobileDropdownOptions() { - this.get("headerComponentOptions") - .set("selectedName", I18n.t(this.get("headerText"))); - }, - - @computed("topic", "topic.details", "value") - content(topic, details) { - const content = []; - - if (details.get("can_invite_to")) { - content.push({ id: "invite", icon: "users", name: I18n.t("topic.invite_reply.title") }); - } - - if (topic.get("bookmarked")) { - content.push({ id: "bookmark", icon: "bookmark", name: I18n.t("bookmarked.clear_bookmarks") }); - } else { - content.push({ id: "bookmark", icon: "bookmark", name: I18n.t("bookmarked.title") }); - } - - content.push({ id: "share", icon: "link", name: I18n.t("topic.share.title") }); - - if (details.get("can_flag_topic")) { - content.push({ id: "flag", icon: "flag", name: I18n.t("topic.flag_topic.title") }); - } - - return content; - }, - - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - - const topic = this.get("topic"); - - // In case it"s not a valid topic - if (!topic.get("id")) { - return; - } - - this.set("value", value); - - const refresh = () => this.set("value", null); - - switch(value) { - case "invite": - this.attrs.showInvite(); - refresh(); - break; - case "bookmark": - topic.toggleBookmark().then(() => refresh() ); - break; - case "share": - this.appEvents.trigger("share:url", topic.get("shareUrl"), $("#topic-footer-buttons")); - refresh(); - break; - case "flag": - this.attrs.showFlagTopic(); - refresh(); - break; - } - } - } -}); diff --git a/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 b/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 deleted file mode 100644 index d8caacb501..0000000000 --- a/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -export default Ember.Mixin.create({ - init() { - this._super(); - - this.offscreenInputSelector = ".select-box-kit-offscreen"; - this.filterInputSelector = ".select-box-kit-filter-input"; - this.rowSelector = ".select-box-kit-row"; - this.collectionSelector = ".select-box-kit-collection"; - this.headerSelector = ".select-box-kit-header"; - this.bodySelector = ".select-box-kit-body"; - }, - - $findRowByValue(value) { - return this.$(`${this.rowSelector}[data-value='${value}']`); - }, - - $header() { - return this.$(this.headerSelector); - }, - - $body() { - return this.$(this.bodySelector); - }, - - $collection() { - return this.$(this.collectionSelector); - }, - - $rows() { - return this.$(this.rowSelector); - }, - - $highlightedRow() { - return this.$rows().filter(".is-highlighted"); - }, - - $selectedRow() { - return this.$rows().filter(".is-selected"); - }, - - $offscreenInput() { - return this.$(this.offscreenInputSelector); - }, - - $filterInput() { - return this.$(this.filterInputSelector); - }, - - _killEvent(event) { - event.preventDefault(); - event.stopPropagation(); - } -}); diff --git a/app/assets/javascripts/select-box-kit/mixins/keyboard.js.es6 b/app/assets/javascripts/select-box-kit/mixins/keyboard.js.es6 deleted file mode 100644 index 0de89dbb2e..0000000000 --- a/app/assets/javascripts/select-box-kit/mixins/keyboard.js.es6 +++ /dev/null @@ -1,203 +0,0 @@ -const { isEmpty } = Ember; - -export default Ember.Mixin.create({ - init() { - this._super(); - - this.keys = { - TAB: 9, - ENTER: 13, - ESC: 27, - SPACE: 32, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - SHIFT: 16, - CTRL: 17, - ALT: 18, - PAGE_UP: 33, - PAGE_DOWN: 34, - HOME: 36, - END: 35, - BACKSPACE: 8 - }; - }, - - willDestroyElement() { - this._super(); - - $(document) - .off("mousedown.select-box-kit") - .off("touchstart.select-box-kit"); - - this.$offscreenInput() - .off("focus.select-box-kit") - .off("focusin.select-box-kit") - .off("blur.select-box-kit") - .off("keydown.select-box-kit"); - - this.$filterInput().off("keydown.select-box-kit"); - }, - - didInsertElement() { - this._super(); - - $(document) - .on("mousedown.select-box-kit, touchstart.select-box-kit", event => { - if (Ember.isNone(this.get("element"))) { - return; - } - - if (this.get("element").contains(event.target)) { return; } - this.clickOutside(event); - }); - - this.$offscreenInput() - .on("blur.select-box-kit", () => { - if (this.get("isExpanded") === false && this.get("isFocused") === true) { - this.close(); - } - }) - .on("focus.select-box-kit", (event) => { - this.set("isFocused", true); - this._killEvent(event); - }) - .on("focusin.select-box-kit", (event) => { - this.set("isFocused", true); - this._killEvent(event); - }) - .on("keydown.select-box-kit", (event) => { - const keyCode = event.keyCode || event.which; - - switch (keyCode) { - case this.keys.UP: - case this.keys.DOWN: - if (this.get("isExpanded") === false) { - this.set("isExpanded", true); - } - - Ember.run.schedule("actions", () => { - this._handleArrowKey(keyCode); - }); - - this._killEvent(event); - - return; - case this.keys.ENTER: - if (this.get("isExpanded") === false) { - this.set("isExpanded", true); - } else { - this.send("onSelect", this.$highlightedRow().data("value")); - } - - this._killEvent(event); - - return; - case this.keys.TAB: - if (this.get("isExpanded") === false) { - return true; - } else { - this.send("onSelect", this.$highlightedRow().data("value")); - return; - } - case this.keys.ESC: - this.close(); - this._killEvent(event); - return; - case this.keys.BACKSPACE: - this._killEvent(event); - return; - } - - if (this._isSpecialKey(keyCode) === false && event.metaKey === false) { - this.setProperties({ - isExpanded: true, - filter: String.fromCharCode(keyCode) - }); - - Ember.run.schedule("afterRender", () => this.$filterInput().focus() ); - } - }); - - this.$filterInput() - .on(`keydown.select-box-kit`, (event) => { - const keyCode = event.keyCode || event.which; - - if ([ - this.keys.RIGHT, - this.keys.LEFT, - this.keys.BACKSPACE, - this.keys.SPACE, - ].includes(keyCode) || event.metaKey === true) { - return true; - } - - if (this._isSpecialKey(keyCode) === true) { - this.$offscreenInput().focus().trigger(event); - } - - return true; - }); - }, - - _handleArrowKey(keyCode) { - if (isEmpty(this.get("filteredContent"))) { - return; - } - - Ember.run.schedule("afterRender", () => { - switch (keyCode) { - case 38: - Ember.run.throttle(this, this._handleUpArrow, 32); - break; - default: - Ember.run.throttle(this, this._handleDownArrow, 32); - } - }); - }, - - _moveHighlight(direction) { - const $rows = this.$rows(); - const currentIndex = $rows.index(this.$highlightedRow()); - - let nextIndex = 0; - - if (currentIndex < 0) { - nextIndex = 0; - } else if (currentIndex + direction < $rows.length) { - nextIndex = currentIndex + direction; - } - - this._rowSelection($rows, nextIndex); - }, - - _handleDownArrow() { this._moveHighlight(1); }, - - _handleUpArrow() { this._moveHighlight(-1); }, - - _rowSelection($rows, nextIndex) { - const highlightableValue = $rows.eq(nextIndex).data("value"); - const $highlightableRow = this.$findRowByValue(highlightableValue); - this.send("onHighlight", highlightableValue); - - Ember.run.schedule("afterRender", () => { - const $collection = this.$collection(); - const currentOffset = $collection.offset().top + - $collection.outerHeight(false); - const nextBottom = $highlightableRow.offset().top + - $highlightableRow.outerHeight(false); - const nextOffset = $collection.scrollTop() + nextBottom - currentOffset; - - if (nextIndex === 0) { - $collection.scrollTop(0); - } else if (nextBottom > currentOffset) { - $collection.scrollTop(nextOffset); - } - }); - }, - - _isSpecialKey(keyCode) { - return _.values(this.keys).includes(keyCode); - }, -}); diff --git a/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 b/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 deleted file mode 100644 index 05d896a832..0000000000 --- a/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 +++ /dev/null @@ -1,9 +0,0 @@ -export default Ember.Mixin.create({ - _castInteger(value) { - if (this.get("castInteger") === true && Ember.isPresent(value)) { - return parseInt(value, 10); - } - - return Ember.isNone(value) ? value : value.toString(); - } -}); diff --git a/app/assets/javascripts/select-box-kit/templates/components/combo-box/combo-box-header.hbs b/app/assets/javascripts/select-box-kit/templates/components/combo-box/combo-box-header.hbs deleted file mode 100644 index 38e070fa34..0000000000 --- a/app/assets/javascripts/select-box-kit/templates/components/combo-box/combo-box-header.hbs +++ /dev/null @@ -1,15 +0,0 @@ -{{#if icon}} - {{{icon}}} -{{/if}} - - - {{{selectedName}}} - - -{{#if shouldDisplayClearableButton}} - -{{/if}} - -{{d-icon caretIcon class="caret-icon"}} diff --git a/app/assets/javascripts/select-box-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs b/app/assets/javascripts/select-box-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs deleted file mode 100644 index 0ffbeb4f86..0000000000 --- a/app/assets/javascripts/select-box-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs +++ /dev/null @@ -1,17 +0,0 @@ - diff --git a/app/assets/javascripts/select-box-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs b/app/assets/javascripts/select-box-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs deleted file mode 100644 index dc936cbe69..0000000000 --- a/app/assets/javascripts/select-box-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#if icon}} -
    - {{{icon}}} -
    -{{/if}} - -{{content.name}} - -{{#if datetime}} - - {{datetime}} - -{{/if}} diff --git a/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/multi-combo-box-header.hbs b/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/multi-combo-box-header.hbs deleted file mode 100644 index 13cf6c3f6d..0000000000 --- a/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/multi-combo-box-header.hbs +++ /dev/null @@ -1,30 +0,0 @@ -
      - {{#each selectedContent as |selectedContent|}} -
    • - - {{d-icon "times"}} - - - {{selectedContent.name}} - -
    • - {{else}} - {{#if shouldDisplayFilterPlaceholder}} -
    • - {{text}} -
    • - {{/if}} - {{/each}} - -
    • - {{input - class="select-box-kit-filter-input" - key-up=onFilterChange - autocomplete="off" - autocorrect="off" - autocapitalize="off" - spellcheck=false - value=filter - }} -
    • -
    diff --git a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit.hbs b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit.hbs deleted file mode 100644 index 6575daf884..0000000000 --- a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit.hbs +++ /dev/null @@ -1,65 +0,0 @@ - - -{{component headerComponent - none=computedNone - isFocused=isFocused - isExpanded=isExpanded - selectedContent=selectedContent - onDeselect=(action "onDeselect") - onToggle=(action "onToggle") - onFilterChange=(action "onFilterChange") - onClearSelection=(action "onClearSelection") - options=headerComponentOptions -}} - -
    - {{component filterComponent - onFilterChange=(action "onFilterChange") - icon=filterIcon - filter=filter - filterable=computedFilterable - isFocused=isFocused - placeholder=(i18n filterPlaceholder) - tabindex=tabindex - }} - - {{#if renderBody}} - {{component collectionComponent - shouldDisplayCreateRow=shouldDisplayCreateRow - none=computedNone - createRowContent=createRowContent - selectedContent=selectedContent - filteredContent=filteredContent - rowComponent=rowComponent - noneRowComponent=noneRowComponent - createRowComponent=createRowComponent - iconForRow=iconForRow - templateForRow=templateForRow - templateForNoneRow=templateForNoneRow - templateForCreateRow=templateForCreateRow - shouldHighlightRow=shouldHighlightRow - shouldSelectRow=shouldSelectRow - titleForRow=titleForRow - onClearSelection=(action "onClearSelection") - onSelect=(action "onSelect") - onHighlight=(action "onHighlight") - onCreateContent=(action "onCreateContent") - noContentLabel=noContentLabel - highlightedValue=highlightedValue - computedValue=computedValue - rowComponentOptions=rowComponentOptions - }} - {{/if}} -
    - -
    diff --git a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-header.hbs b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-header.hbs deleted file mode 100644 index b0cea9f913..0000000000 --- a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-header.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#if icon}} - {{{icon}}} -{{/if}} - - - {{{selectedName}}} - diff --git a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-row.hbs b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-row.hbs deleted file mode 100644 index 1786cfb553..0000000000 --- a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-row.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#if template}} - {{{template}}} -{{else}} - {{#if icon}} - {{{icon}}} - {{/if}} - - {{content.name}} -{{/if}} diff --git a/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6 new file mode 100644 index 0000000000..55708f10ea --- /dev/null +++ b/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6 @@ -0,0 +1,87 @@ +import DropdownSelectBox from "select-kit/components/dropdown-select-box"; +import computed from "ember-addons/ember-computed-decorators"; + +export default DropdownSelectBox.extend({ + pluginApiIdentifiers: ["admin-agree-flag-dropdown"], + classNames: ["agree-flag", "admin-agree-flag-dropdown"], + adminTools: Ember.inject.service(), + nameProperty: "label", + allowInitialValueMutation: false, + headerIcon: "thumbs-o-up", + + computeHeaderContent() { + let content = this.baseHeaderComputedContent(); + content.name = `${I18n.t("admin.flags.agree")}...`; + return content; + }, + + @computed("adminTools", "post.user") + spammerDetails(adminTools, user) { + return adminTools.spammerDetails(user); + }, + + canDeleteSpammer: Ember.computed.and("spammerDetails.canDelete", "post.flaggedForSpam"), + + computeContent() { + const content = []; + const post = this.get("post"); + const canDeleteSpammer = this.get("canDeleteSpammer"); + + if (post.user_deleted) { + content.push({ + icon: "eye", + id: "confirm-agree-restore", + action: () => this.send("perform", "restore"), + label: I18n.t("admin.flags.agree_flag_restore_post"), + description: I18n.t("admin.flags.agree_flag_restore_post_title") + }); + } else { + if (!post.get("postHidden")) { + content.push({ + icon: "eye-slash", + action: () => this.send("perform", "hide"), + id: "confirm-agree-hide", + label: I18n.t("admin.flags.agree_flag_hide_post"), + description: I18n.t("admin.flags.agree_flag_hide_post_title") + }); + } + } + + content.push({ + icon: "thumbs-o-up", + id: "confirm-agree-keep", + description: I18n.t('admin.flags.agree_flag_title'), + action: () => this.send("perform", "keep"), + label: I18n.t("admin.flags.agree_flag"), + }); + + if (canDeleteSpammer) { + content.push({ + title: I18n.t("admin.flags.delete_spammer_title"), + icon: "exclamation-triangle", + id: "delete-spammer", + action: () => this.send("deleteSpammer"), + label: I18n.t("admin.flags.delete_spammer"), + }); + } + + return content; + }, + + mutateValue(value) { + const computedContentItem = this.get("computedContent").findBy("value", value); + Ember.get(computedContentItem, "originalContent.action")(); + }, + + actions: { + deleteSpammer() { + let spammerDetails = this.get("spammerDetails"); + this.attrs.removeAfter(spammerDetails.deleteUser()); + }, + + perform(action) { + let flaggedPost = this.get("post"); + this.attrs.removeAfter(flaggedPost.agreeFlags(action)); + }, + } +}); diff --git a/app/assets/javascripts/select-kit/components/admin-delete-flag-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/admin-delete-flag-dropdown.js.es6 new file mode 100644 index 0000000000..2144a1512a --- /dev/null +++ b/app/assets/javascripts/select-kit/components/admin-delete-flag-dropdown.js.es6 @@ -0,0 +1,77 @@ +import DropdownSelectBox from "select-kit/components/dropdown-select-box"; +import computed from "ember-addons/ember-computed-decorators"; + +export default DropdownSelectBox.extend({ + classNames: ["delete-flag", "admin-delete-flag-dropdown"], + adminTools: Ember.inject.service(), + nameProperty: "label", + headerIcon: "trash-o", + + computeHeaderContent() { + let content = this.baseHeaderComputedContent(); + content.name = I18n.t("admin.flags.delete"); + return content; + }, + + @computed("adminTools", "post.user") + spammerDetails(adminTools, user) { + return adminTools.spammerDetails(user); + }, + + canDeleteSpammer: Ember.computed.and("spammerDetails.canDelete", "post.flaggedForSpam"), + + computeContent() { + const content = []; + const canDeleteSpammer = this.get("canDeleteSpammer"); + + content.push({ + icon: "external-link", + id: "delete-defer", + action: () => this.send("deletePostDeferFlag"), + label: I18n.t("admin.flags.delete_post_defer_flag"), + description: I18n.t("admin.flags.delete_post_defer_flag_title"), + }); + + content.push({ + icon: "thumbs-o-up", + id: "delete-agree", + action: () => this.send("deletePostAgreeFlag"), + label: I18n.t("admin.flags.delete_post_agree_flag"), + description: I18n.t("admin.flags.delete_post_agree_flag_title"), + }); + + if (canDeleteSpammer) { + content.push({ + title: I18n.t("admin.flags.delete_post_agree_flag_title"), + icon: "exclamation-triangle", + id: "delete-spammer", + action: () => this.send("deleteSpammer"), + label: I18n.t("admin.flags.delete_spammer") + }); + } + + return content; + }, + + mutateValue(value) { + const computedContentItem = this.get("computedContent").findBy("value", value); + Ember.get(computedContentItem, "originalContent.action")(); + }, + + actions: { + deleteSpammer() { + let spammerDetails = this.get("spammerDetails"); + this.attrs.removeAfter(spammerDetails.deleteUser()); + }, + + deletePostDeferFlag() { + let flaggedPost = this.get('post'); + this.attrs.removeAfter(flaggedPost.deferFlags(true)); + }, + + deletePostAgreeFlag() { + let flaggedPost = this.get('post'); + this.attrs.removeAfter(flaggedPost.agreeFlags('delete')); + } + } +}); diff --git a/app/assets/javascripts/select-kit/components/admin-group-selector.js.es6 b/app/assets/javascripts/select-kit/components/admin-group-selector.js.es6 new file mode 100644 index 0000000000..be3352b376 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/admin-group-selector.js.es6 @@ -0,0 +1,51 @@ +import MultiSelectComponent from "select-kit/components/multi-select"; +const { makeArray } = Ember; + +export default MultiSelectComponent.extend({ + pluginApiIdentifiers: ["admin-group-selector"], + classNames: "admin-group-selector", + selected: null, + available: null, + allowAny: false, + + computeValues() { + return makeArray(this.get("selected")) + .map(s => this.valueForContentItem(s)); + }, + + computeContent() { + return makeArray(this.get("available")); + }, + + computeContentItem(contentItem, name) { + let computedContent = this.baseComputedContentItem(contentItem, name); + computedContent.locked = contentItem.automatic; + return computedContent; + }, + + mutateValues(values) { + if (values.length > this.get("selected").length) { + const newValues = values + .filter(v => !this.get("selected") + .map(s => this.valueForContentItem(s)) + .includes(v)); + + newValues.forEach(value => { + const actionContext = this.get("available") + .findBy(this.get("valueAttribute"), parseInt(value, 10)); + + this.triggerAction({ action: "groupAdded", actionContext }); + }); + } else if (values.length < this.get("selected").length) { + const selected = this.get("selected") + .filter(s => !values.includes(this.valueForContentItem(s))); + + selected.forEach(s => { + this.triggerAction({ + action: "groupRemoved", + actionContext: this.valueForContentItem(s) + }); + }); + } + } +}); diff --git a/app/assets/javascripts/select-kit/components/categories-admin-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/categories-admin-dropdown.js.es6 new file mode 100644 index 0000000000..63f1bac549 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/categories-admin-dropdown.js.es6 @@ -0,0 +1,38 @@ +import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box"; + +export default DropdownSelectBoxComponent.extend({ + pluginApiIdentifiers: ["categories-admin-dropdown"], + classNames: "categories-admin-dropdown", + showFullTitle: false, + allowInitialValueMutation: false, + headerIcon: ["bars", "caret-down"], + + autoHighlight() {}, + + computeContent() { + const items = [ + { + id: "create", + name: I18n.t("category.create"), + description: I18n.t("category.create_long"), + icon: "plus" + } + ]; + + const includeReorder = this.get("siteSettings.fixed_category_positions"); + if (includeReorder) { + items.push({ + id: "reorder", + name: I18n.t("categories.reorder.title"), + description: I18n.t("categories.reorder.title_long"), + icon: "random" + }); + } + + return items; + }, + + mutateValue(value) { + this.get(value)(); + } +}); diff --git a/app/assets/javascripts/select-kit/components/category-chooser.js.es6 b/app/assets/javascripts/select-kit/components/category-chooser.js.es6 new file mode 100644 index 0000000000..47a319e066 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/category-chooser.js.es6 @@ -0,0 +1,92 @@ +import ComboBoxComponent from "select-kit/components/combo-box"; +import { on } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; +import PermissionType from "discourse/models/permission-type"; +import Category from "discourse/models/category"; +const { get, isNone, isEmpty } = Ember; + +export default ComboBoxComponent.extend({ + pluginApiIdentifiers: ["category-chooser"], + classNames: "category-chooser", + filterable: true, + castInteger: true, + allowUncategorized: false, + rowComponent: "category-row", + noneRowComponent: "none-category-row", + allowSubCategories: true, + + filterComputedContent(computedContent, computedValue, filter) { + if (isEmpty(filter)) { return computedContent; } + + const _matchFunction = (f, text) => { + return text.toLowerCase().indexOf(f) > -1; + }; + const lowerFilter = filter.toLowerCase(); + + return computedContent.filter(c => { + const category = Category.findById(get(c, "value")); + const text = get(c, "name"); + if (category && category.get("parentCategory")) { + const categoryName = category.get("parentCategory.name"); + return _matchFunction(lowerFilter, text) || _matchFunction(lowerFilter, categoryName); + } else { + return _matchFunction(lowerFilter, text); + } + }); + }, + + @computed("rootNone", "rootNoneLabel") + none(rootNone, rootNoneLabel) { + if (this.siteSettings.allow_uncategorized_topics || this.get("allowUncategorized")) { + if (!isNone(rootNone)) { + return rootNoneLabel || "category.none"; + } else { + return Category.findUncategorized(); + } + } else { + return "category.choose"; + } + }, + + @on("didRender") + _bindComposerResizing() { + this.appEvents.on("composer:resized", this, this.applyDirection); + }, + + @on("willDestroyElement") + _unbindComposerResizing() { + this.appEvents.off("composer:resized"); + }, + + computeContent() { + const categories = Discourse.SiteSettings.fixed_category_positions_on_create ? + Category.list() : + Category.listByActivity(); + + let scopedCategoryId = this.get("scopedCategoryId"); + if (scopedCategoryId) { + const scopedCat = Category.findById(scopedCategoryId); + scopedCategoryId = scopedCat.get("parent_category_id") || scopedCat.get("id"); + } + + const excludeCategoryId = this.get("excludeCategoryId"); + + return categories.filter(c => { + const categoryId = this.valueForContentItem(c); + + if (scopedCategoryId && categoryId !== scopedCategoryId && get(c, "parent_category_id") !== scopedCategoryId) { + return false; + } + + if (this.get("allowSubCategories") === false && c.get("parentCategory") ) { + return false; + } + + if ((this.get("allowUncategorized") === false && get(c, "isUncategorizedCategory")) || excludeCategoryId === categoryId) { + return false; + } + + return get(c, "permission") === PermissionType.FULL; + }); + } +}); diff --git a/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 b/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 new file mode 100644 index 0000000000..f84c0ba4c2 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 @@ -0,0 +1,20 @@ +import NotificationOptionsComponent from "select-kit/components/notifications-button"; +import computed from "ember-addons/ember-computed-decorators"; + +export default NotificationOptionsComponent.extend({ + pluginApiIdentifiers: ["category-notifications-button"], + classNames: "category-notifications-button", + isHidden: Ember.computed.or("category.deleted", "site.isMobileDevice"), + i18nPrefix: "category.notifications", + showFullTitle: false, + allowInitialValueMutation: false, + + mutateValue(value) { + this.get("category").setNotification(value); + }, + + @computed("iconForSelectedDetails") + headerIcon(iconForSelectedDetails) { + return [iconForSelectedDetails, "caret-down"]; + } +}); diff --git a/app/assets/javascripts/select-kit/components/category-row.js.es6 b/app/assets/javascripts/select-kit/components/category-row.js.es6 new file mode 100644 index 0000000000..da59a4cfe9 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/category-row.js.es6 @@ -0,0 +1,71 @@ +import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row"; +import computed from "ember-addons/ember-computed-decorators"; +import Category from "discourse/models/category"; +import { categoryBadgeHTML } from "discourse/helpers/category-link"; + +export default SelectKitRowComponent.extend({ + layoutName: "select-kit/templates/components/category-row", + classNames: "category-row", + + @computed("options.displayCategoryDescription") + displayCategoryDescription(displayCategoryDescription) { + if (Ember.isNone(displayCategoryDescription)) { + return true; + } + + return displayCategoryDescription; + }, + + @computed("computedContent.value", "computedContent.name") + category(value, name) { + if (Ember.isEmpty(value)) { + const uncat = Category.findUncategorized(); + if (uncat && uncat.get("name") === name) { + return uncat; + } + } else { + return Category.findById(parseInt(value, 10)); + } + }, + + @computed("category") + badgeForCategory(category) { + return categoryBadgeHTML(category, { + link: false, + allowUncategorized: true, + hideParent: true + }).htmlSafe(); + }, + + @computed("parentCategory") + badgeForParentCategory(parentCategory) { + return categoryBadgeHTML(parentCategory, {link: false}).htmlSafe(); + }, + + @computed("parentCategoryid") + parentCategory(parentCategoryId) { + return Category.findById(parentCategoryId); + }, + + @computed("parentCategoryid") + hasParentCategory(parentCategoryid) { + return !Ember.isNone(parentCategoryid); + }, + + @computed("category") + parentCategoryid(category) { + return category.get("parent_category_id"); + }, + + topicCount: Ember.computed.alias("category.topic_count"), + + @computed("displayCategoryDescription", "category.description") + shouldDisplayDescription(displayCategoryDescription, description) { + return displayCategoryDescription && description && description !== "null"; + }, + + @computed("category.description") + description(description) { + return `${description.substr(0, 200)}${description.length > 200 ? '…' : ''}`; + } +}); diff --git a/app/assets/javascripts/select-kit/components/category-selector.js.es6 b/app/assets/javascripts/select-kit/components/category-selector.js.es6 new file mode 100644 index 0000000000..63e58e3d30 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/category-selector.js.es6 @@ -0,0 +1,40 @@ +import MultiSelectComponent from "select-kit/components/multi-select"; +import Category from "discourse/models/category"; + +export default MultiSelectComponent.extend({ + pluginApiIdentifiers: ["category-selector"], + classNames: "category-selector", + filterable: true, + allowAny: false, + rowComponent: "category-row", + + init() { + this._super(); + + this.get("headerComponentOptions").setProperties({ + selectedNameComponent: "multi-select/selected-category" + }); + + this.get("rowComponentOptions").setProperties({ + displayCategoryDescription: false + }); + }, + + computeValues() { + return Ember.makeArray(this.get("categories")).map(c => c.id); + }, + + mutateValues(values) { + this.set("categories", values.map(v => Category.findById(v))); + }, + + filterComputedContent(computedContent, computedValues, filter) { + const regex = new RegExp(filter.toLowerCase(), 'i'); + return computedContent.filter(category => Ember.get(category, "name").match(regex)); + }, + + computeContent() { + const blacklist = Ember.makeArray(this.get("blacklist")); + return Category.list().filter(category => !blacklist.includes(category)); + } +}); diff --git a/app/assets/javascripts/select-box-kit/components/combo-box.js.es6 b/app/assets/javascripts/select-kit/components/combo-box.js.es6 similarity index 53% rename from app/assets/javascripts/select-box-kit/components/combo-box.js.es6 rename to app/assets/javascripts/select-kit/components/combo-box.js.es6 index f5ff7d0305..0a576eae9a 100644 --- a/app/assets/javascripts/select-box-kit/components/combo-box.js.es6 +++ b/app/assets/javascripts/select-kit/components/combo-box.js.es6 @@ -1,7 +1,8 @@ -import SelectBoxKitComponent from "select-box-kit/components/select-box-kit"; +import SingleSelectComponent from "select-kit/components/single-select"; import { on } from "ember-addons/ember-computed-decorators"; -export default SelectBoxKitComponent.extend({ +export default SingleSelectComponent.extend({ + pluginApiIdentifiers: ["combo-box"], classNames: "combobox combo-box", autoFilterable: true, headerComponent: "combo-box/combo-box-header", @@ -10,12 +11,18 @@ export default SelectBoxKitComponent.extend({ caretDownIcon: "caret-down", clearable: false, + computeHeaderContent() { + let content = this.baseHeaderComputedContent(); + content.hasSelection = this.get("hasSelection"); + return content; + }, + @on("didReceiveAttrs") _setComboBoxOptions() { - this.set("headerComponentOptions", Ember.Object.create({ + this.get("headerComponentOptions").setProperties({ caretUpIcon: this.get("caretUpIcon"), caretDownIcon: this.get("caretDownIcon"), clearable: this.get("clearable"), - })); + }); } }); diff --git a/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 b/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 new file mode 100644 index 0000000000..6cc5ecd21f --- /dev/null +++ b/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 @@ -0,0 +1,21 @@ +import SelectKitHeaderComponent from "select-kit/components/select-kit/select-kit-header"; +import { default as computed } from "ember-addons/ember-computed-decorators"; + +export default SelectKitHeaderComponent.extend({ + layoutName: "select-kit/templates/components/combo-box/combo-box-header", + classNames: "combo-box-header", + + clearable: Ember.computed.alias("options.clearable"), + caretUpIcon: Ember.computed.alias("options.caretUpIcon"), + caretDownIcon: Ember.computed.alias("options.caretDownIcon"), + + @computed("isExpanded", "caretUpIcon", "caretDownIcon") + caretIcon(isExpanded, caretUpIcon, caretDownIcon) { + return isExpanded === true ? caretUpIcon : caretDownIcon; + }, + + @computed("clearable", "computedContent.hasSelection") + shouldDisplayClearableButton(clearable, hasSelection) { + return clearable === true && hasSelection === true; + } +}); diff --git a/app/assets/javascripts/select-kit/components/dropdown-select-box.js.es6 b/app/assets/javascripts/select-kit/components/dropdown-select-box.js.es6 new file mode 100644 index 0000000000..8e7636445a --- /dev/null +++ b/app/assets/javascripts/select-kit/components/dropdown-select-box.js.es6 @@ -0,0 +1,32 @@ +import SingleSelectComponent from "select-kit/components/single-select"; +import { on } from "ember-addons/ember-computed-decorators"; + +export default SingleSelectComponent.extend({ + pluginApiIdentifiers: ["dropdown-select-box"], + classNames: "dropdown-select-box", + verticalOffset: 3, + fullWidthOnMobile: true, + filterable: false, + autoFilterable: false, + headerComponent: "dropdown-select-box/dropdown-select-box-header", + rowComponent: "dropdown-select-box/dropdown-select-box-row", + showFullTitle: true, + allowInitialValueMutation: false, + + @on("didReceiveAttrs") + _setDropdownSelectBoxComponentOptions() { + this.get("headerComponentOptions").setProperties({ + showFullTitle: this.get("showFullTitle") + }); + }, + + didClickOutside() { + if (this.get("isExpanded") === false) { return; } + this.close(); + }, + + didSelect() { + this._super(); + this.close(); + } +}); diff --git a/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 new file mode 100644 index 0000000000..d7e0876430 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 @@ -0,0 +1,15 @@ +import SelectKitHeaderComponent from "select-kit/components/select-kit/select-kit-header"; +import computed from "ember-addons/ember-computed-decorators"; + +export default SelectKitHeaderComponent.extend({ + layoutName: "select-kit/templates/components/dropdown-select-box/dropdown-select-box-header", + classNames: "dropdown-select-box-header", + tagName: "button", + + classNameBindings: ["btnClassName"], + + @computed("options.showFullTitle") + btnClassName(showFullTitle) { + return `btn ${showFullTitle ? 'btn-icon-text' : 'no-text btn-icon'}`; + } +}); diff --git a/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 new file mode 100644 index 0000000000..fd865fb602 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-row.js.es6 @@ -0,0 +1,9 @@ +import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row"; + +export default SelectKitRowComponent.extend({ + layoutName: "select-kit/templates/components/dropdown-select-box/dropdown-select-box-row", + classNames: "dropdown-select-box-row", + + name: Ember.computed.alias("computedContent.name"), + description: Ember.computed.alias("computedContent.originalContent.description") +}); diff --git a/app/assets/javascripts/select-box-kit/components/future-date-input-selector.js.es6 b/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 similarity index 74% rename from app/assets/javascripts/select-box-kit/components/future-date-input-selector.js.es6 rename to app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 index b344f2080e..8597cb9668 100644 --- a/app/assets/javascripts/select-box-kit/components/future-date-input-selector.js.es6 +++ b/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 @@ -1,7 +1,6 @@ -import { default as computed, observes } from "ember-addons/ember-computed-decorators"; -import ComboBoxComponent from "select-box-kit/components/combo-box"; +import ComboBoxComponent from "select-kit/components/combo-box"; import { CLOSE_STATUS_TYPE } from "discourse/controllers/edit-topic-timer"; -import DatetimeMixin from "select-box-kit/components/future-date-input-selector/mixin"; +import DatetimeMixin from "select-kit/components/future-date-input-selector/mixin"; const TIMEFRAME_BASE = { enabled: () => true, @@ -109,17 +108,34 @@ export function timeframeDetails(id) { return _timeframeById[id]; } -export const FORMAT = "YYYY-MM-DD HH:mm"; +export const FORMAT = "YYYY-MM-DD HH:mmZ"; export default ComboBoxComponent.extend(DatetimeMixin, { + pluginApiIdentifiers: ["future-date-input-selector"], classNames: ["future-date-input-selector"], isCustom: Ember.computed.equal("value", "pick_date_and_time"), + isBasedOnLastPost: Ember.computed.equal("value", "set_based_on_last_post"), clearable: true, rowComponent: "future-date-input-selector/future-date-input-selector-row", headerComponent: "future-date-input-selector/future-date-input-selector-header", - @computed - content() { + computeHeaderContent() { + let content = this.baseHeaderComputedContent(); + content.datetime = this._computeDatetimeForValue(this.get("computedValue")); + content.name = this.get("selectedComputedContent.name") || content.name; + content.hasSelection = this.get("hasSelection"); + content.icons = this._computeIconsForValue(this.get("computedValue")); + return content; + }, + + computeContentItem(contentItem, name) { + let item = this.baseComputedContentItem(contentItem, name); + item.datetime = this._computeDatetimeForValue(contentItem.id); + item.icons = this._computeIconsForValue(contentItem.id); + return item; + }, + + computeContent() { let now = moment(); let opts = { now, @@ -138,21 +154,18 @@ export default ComboBoxComponent.extend(DatetimeMixin, { }); }, - @observes("value") - _updateInput() { - if (this.get("isCustom")) return; - let input = null; - const { time } = this.get("updateAt"); + mutateValue(value) { + if (this.get("isCustom") || this.get("isBasedOnLastPost")) { + this.set("value", value); + } else { + let input = null; + const { time } = this._updateAt(value); - if (time && !Ember.isEmpty(this.get("value"))) { - input = time.format(FORMAT); + if (time && !Ember.isEmpty(value)) { + input = time.format(FORMAT); + } + + this.setProperties({ input, value }); } - - this.set("input", input); }, - - @computed("value") - updateAt(value) { - return this._updateAt(value); - } }); diff --git a/app/assets/javascripts/select-kit/components/future-date-input-selector/future-date-input-selector-header.js.es6 b/app/assets/javascripts/select-kit/components/future-date-input-selector/future-date-input-selector-header.js.es6 new file mode 100644 index 0000000000..7a5e807538 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/future-date-input-selector/future-date-input-selector-header.js.es6 @@ -0,0 +1,6 @@ +import ComboBoxHeaderComponent from "select-kit/components/combo-box/combo-box-header"; + +export default ComboBoxHeaderComponent.extend({ + layoutName: "select-kit/templates/components/future-date-input-selector/future-date-input-selector-header", + classNames: "future-date-input-selector-header" +}); diff --git a/app/assets/javascripts/select-kit/components/future-date-input-selector/future-date-input-selector-row.js.es6 b/app/assets/javascripts/select-kit/components/future-date-input-selector/future-date-input-selector-row.js.es6 new file mode 100644 index 0000000000..a04a59507b --- /dev/null +++ b/app/assets/javascripts/select-kit/components/future-date-input-selector/future-date-input-selector-row.js.es6 @@ -0,0 +1,6 @@ +import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row"; + +export default SelectKitRowComponent.extend({ + layoutName: "select-kit/templates/components/future-date-input-selector/future-date-input-selector-row", + classNames: "future-date-input-selector-row" +}); diff --git a/app/assets/javascripts/select-box-kit/components/future-date-input-selector/mixin.js.es6 b/app/assets/javascripts/select-kit/components/future-date-input-selector/mixin.js.es6 similarity index 76% rename from app/assets/javascripts/select-box-kit/components/future-date-input-selector/mixin.js.es6 rename to app/assets/javascripts/select-kit/components/future-date-input-selector/mixin.js.es6 index d69b38a566..6368957056 100644 --- a/app/assets/javascripts/select-box-kit/components/future-date-input-selector/mixin.js.es6 +++ b/app/assets/javascripts/select-kit/components/future-date-input-selector/mixin.js.es6 @@ -1,16 +1,15 @@ -import { iconHTML } from 'discourse-common/lib/icon-library'; import { CLOSE_STATUS_TYPE } from 'discourse/controllers/edit-topic-timer'; -import { timeframeDetails } from 'select-box-kit/components/future-date-input-selector'; +import { timeframeDetails } from 'select-kit/components/future-date-input-selector'; export default Ember.Mixin.create({ - _computeIconForValue(value) { + _computeIconsForValue(value) { let {icon} = this._updateAt(value); if (icon) { - return icon.split(",").map(i => iconHTML(i)).join(" "); + return icon.split(","); } - return null; + return []; }, _computeDatetimeForValue(value) { @@ -20,7 +19,6 @@ export default Ember.Mixin.create({ let {time} = this._updateAt(value); if (time) { - let details = timeframeDetails(value); if (!details.displayWhen) { time = null; @@ -34,6 +32,7 @@ export default Ember.Mixin.create({ _updateAt(selection) { let details = timeframeDetails(selection); + if (details) { return { time: details.when(moment(), this.get('statusType') !== CLOSE_STATUS_TYPE ? 8 : 18), diff --git a/app/assets/javascripts/select-kit/components/group-notifications-button.js.es6 b/app/assets/javascripts/select-kit/components/group-notifications-button.js.es6 new file mode 100644 index 0000000000..8236e4bbe1 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/group-notifications-button.js.es6 @@ -0,0 +1,12 @@ +import NotificationOptionsComponent from "select-kit/components/notifications-button"; + +export default NotificationOptionsComponent.extend({ + pluginApiIdentifiers: ["grouo-notifications-button"], + classNames: ["group-notifications-button"], + i18nPrefix: "groups.notifications", + allowInitialValueMutation: false, + + mutateValue(value) { + this.get("group").setNotification(value, this.get("user.id")); + } +}); diff --git a/app/assets/javascripts/select-kit/components/list-setting.js.es6 b/app/assets/javascripts/select-kit/components/list-setting.js.es6 new file mode 100644 index 0000000000..0872aab951 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/list-setting.js.es6 @@ -0,0 +1,54 @@ +import MultiSelectComponent from "select-kit/components/multi-select"; + +export default MultiSelectComponent.extend({ + pluginApiIdentifiers: ["list-setting"], + classNames: "list-setting", + tokenSeparator: "|", + settingValue: "", + choices: null, + filterable: true, + + init() { + this._super(); + + if (!Ember.isNone(this.get("settingName"))) { + this.set("nameProperty", this.get("settingName")); + } + + if (this.get("nameProperty").indexOf("color") > -1) { + this.set("headerComponentOptions", Ember.Object.create({ + selectedNameComponent: "multi-select/selected-color" + })); + } + }, + + computeContent() { + let content; + if (Ember.isNone(this.get("choices"))) { + content = this.get("settingValue").split(this.get("tokenSeparator"));; + } else { + content = this.get("choices"); + } + + return Ember.makeArray(content).filter(c => c); + }, + + mutateValues(values) { + this.set("settingValue", values.join(this.get("tokenSeparator"))); + }, + + computeValues() { + return this.get("settingValue") + .split(this.get("tokenSeparator")) + .filter(c => c); + }, + + _handleTabOnKeyDown(event) { + if (this.$highlightedRow().length === 1) { + this._super(event); + } else { + this.close(); + return false; + } + } +}); diff --git a/app/assets/javascripts/select-kit/components/multi-select.js.es6 b/app/assets/javascripts/select-kit/components/multi-select.js.es6 new file mode 100644 index 0000000000..279656145a --- /dev/null +++ b/app/assets/javascripts/select-kit/components/multi-select.js.es6 @@ -0,0 +1,266 @@ +import SelectKitComponent from "select-kit/components/select-kit"; +import computed from "ember-addons/ember-computed-decorators"; +import { on } from "ember-addons/ember-computed-decorators"; +const { get, isNone, isEmpty, makeArray } = Ember; +import { + applyOnSelectPluginApiCallbacks +} from "select-kit/mixins/plugin-api"; + +export default SelectKitComponent.extend({ + pluginApiIdentifiers: ["multi-select"], + classNames: "multi-select", + headerComponent: "multi-select/multi-select-header", + filterComponent: null, + headerText: "select_kit.default_header_text", + allowAny: true, + allowInitialValueMutation: false, + autoFilterable: true, + selectedNameComponent: "multi-select/selected-name", + + init() { + this._super(); + + this.set("computedValues", []); + if (isNone(this.get("values"))) { this.set("values", []); } + + this.set("headerComponentOptions", Ember.Object.create({ + selectedNameComponent: this.get("selectedNameComponent") + })); + }, + + @on("didRender") + _setChoicesMaxWidth() { + const width = this.$body().outerWidth(false); + this.$(".choices").css({ maxWidth: width, width }); + }, + + @on("didReceiveAttrs") + _compute() { + Ember.run.scheduleOnce("afterRender", () => { + this.willComputeAttributes(); + let content = this.willComputeContent(this.get("content") || []); + let values = this._beforeWillComputeValues(this.get("values")); + content = this.computeContent(content); + content = this._beforeDidComputeContent(content); + values = this.willComputeValues(values); + values = this.computeValues(values); + values = this._beforeDidComputeValues(values); + this.set("headerComputedContent", this.computeHeaderContent()); + this.didComputeContent(content); + this.didComputeValues(values); + this.didComputeAttributes(); + }); + }, + + @computed("filter", "shouldDisplayCreateRow") + createRowComputedContent(filter, shouldDisplayCreateRow) { + if (shouldDisplayCreateRow === true) { + let content = this.createContentFromInput(filter); + return this.computeContentItem(content, { created: true }); + } + }, + + @computed("filter", "computedValues") + shouldDisplayCreateRow(filter, computedValues) { + return this._super() && !computedValues.includes(filter); + }, + + @computed + shouldDisplayFilter() { return true; }, + + _beforeWillComputeValues(values) { + return values.map(v => this._castInteger(v === "" ? null : v)); + }, + willComputeValues(values) { return values; }, + computeValues(values) { return values; }, + _beforeDidComputeValues(values) { + this.setProperties({ computedValues: values }); + return values; + }, + didComputeValues(values) { return values; }, + + mutateAttributes() { + if (this.get("isDestroyed") || this.get("isDestroying")) return; + + Ember.run.next(() => { + this.mutateContent(this.get("computedContent")); + this.mutateValues(this.get("computedValues")); + applyOnSelectPluginApiCallbacks(this.get("pluginApiIdentifiers"), this.get("computedValues"), this); + this.set("headerComputedContent", this.computeHeaderContent()); + }); + }, + mutateValues(computedValues) { + this.set("values", computedValues); + }, + mutateContent() { }, + + filterComputedContent(computedContent, computedValues, filter) { + const lowerFilter = filter.toLowerCase(); + return computedContent.filter(c => { + return get(c, "name").toLowerCase().indexOf(lowerFilter) > -1; + }); + }, + + @computed("computedContent.[]", "computedValues.[]", "filter") + filteredComputedContent(computedContent, computedValues, filter) { + computedContent = computedContent.filter(c => { + return !computedValues.includes(get(c, "value")); + }); + + if (this.get("shouldFilter") === true) { + computedContent = this.filterComputedContent(computedContent, computedValues, filter); + } + + return computedContent.slice(0, this.get("limitMatches")); + }, + + baseHeaderComputedContent() { + return { + selectedComputedContents: this.get("selectedComputedContents") + }; + }, + + @computed("filter") + templateForCreateRow() { + return (rowComponent) => { + return I18n.t("select_kit.create", { content: rowComponent.get("computedContent.name")}); + }; + }, + + didPressBackspace(event) { + this.expand(event); + this.keyDown(event); + this._destroyEvent(event); + }, + + didPressEscape(event) { + const $highlighted = this.$(".selected-name.is-highlighted"); + if ($highlighted.length > 0) { + $highlighted.removeClass("is-highlighted"); + } + + this._super(event); + }, + + keyDown(event) { + if (!isEmpty(this.get("filter"))) return; + + const keyCode = event.keyCode || event.which; + const $filterInput = this.$filterInput(); + + // select all choices + if (this.get("hasSelection") && event.metaKey === true && keyCode === 65) { + this.$(".choices .selected-name:not(.is-locked)").addClass("is-highlighted"); + return false; + } + + // clear selection when multiple + if (this.$(".selected-name.is-highlighted").length >= 1 && keyCode === this.keys.BACKSPACE) { + const highlightedComputedContents = []; + $.each(this.$(".selected-name.is-highlighted"), (i, el) => { + const computedContent = this._findComputedContentItemByGuid($(el).attr("data-guid")); + if (!Ember.isNone(computedContent)) { highlightedComputedContents.push(computedContent); } + }); + this.send("onDeselect", highlightedComputedContents); + return; + } + + // try to remove last item from the list + if (keyCode === this.keys.BACKSPACE) { + let $lastSelectedValue = $(this.$(".choices .selected-name:not(.is-locked)").last()); + + if ($lastSelectedValue.length === 0) { return; } + + if ($filterInput.not(":visible") && $lastSelectedValue.length > 0) { + $lastSelectedValue.click(); + return false; + } + + if ($filterInput.val() === "") { + if ($filterInput.is(":focus")) { + if ($lastSelectedValue.length > 0) { $lastSelectedValue.click(); } + } else { + if ($lastSelectedValue.length > 0) { + $lastSelectedValue.click(); + } else { + $filterInput.focus(); + } + } + } + } + }, + + @computed("computedValues.[]", "computedContent.[]") + selectedComputedContents(computedValues, computedContent) { + const selected = []; + computedValues.forEach(v => selected.push(computedContent.findBy("value", v)) ); + return selected; + }, + + @computed("selectedComputedContents.[]") + hasSelection(selectedComputedContents) { return !Ember.isEmpty(selectedComputedContents); }, + + autoHighlight() { + Ember.run.schedule("afterRender", () => { + if (this.get("isExpanded") === false) { return; } + if (this.get("renderedBodyOnce") === false) { return; } + if (!isNone(this.get("highlightedValue"))) { return; } + + if (isEmpty(this.get("filteredComputedContent"))) { + if (this.get("createRowComputedContent")) { + this.send("onHighlight", this.get("createRowComputedContent")); + } else if (this.get("noneRowComputedContent") && this.get("hasSelection") === true) { + this.send("onHighlight", this.get("noneRowComputedContent")); + } + } else { + this.send("onHighlight", this.get("filteredComputedContent.firstObject")); + } + }); + }, + + didSelect() { + this.focusFilterOrHeader(); + this.autoHighlight(); + }, + + didDeselect() { + this.focusFilterOrHeader(); + this.autoHighlight(); + }, + + validateComputedContentItem(computedContentItem) { + return !this.get("computedValues").includes(computedContentItem.value); + }, + + actions: { + onClear() { + this.get("selectedComputedContents").forEach(selectedComputedContent => { + this.send("onDeselect", selectedComputedContent); + }); + }, + + onCreate(computedContentItem) { + if (this.validateComputedContentItem(computedContentItem)) { + this.get("computedContent").pushObject(computedContentItem); + this.send("onSelect", computedContentItem); + } + }, + + onSelect(computedContentItem) { + this.willSelect(computedContentItem); + this.get("computedValues").pushObject(computedContentItem.value); + Ember.run.next(() => this.mutateAttributes()); + Ember.run.schedule("afterRender", () => this.didSelect(computedContentItem)); + }, + + onDeselect(rowComputedContentItems) { + rowComputedContentItems = Ember.makeArray(rowComputedContentItems); + const generatedComputedContents = this._filterRemovableComputedContents(makeArray(rowComputedContentItems)); + this.willDeselect(rowComputedContentItems); + this.get("computedValues").removeObjects(rowComputedContentItems.map(r => r.value)); + this.get("computedContent").removeObjects(generatedComputedContents); + Ember.run.next(() => this.mutateAttributes()); + Ember.run.schedule("afterRender", () => this.didDeselect(rowComputedContentItems)); + } + } +}); diff --git a/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 new file mode 100644 index 0000000000..d86af77ed8 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 @@ -0,0 +1,32 @@ +import { on } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; +import SelectKitHeaderComponent from "select-kit/components/select-kit/select-kit-header"; + +export default SelectKitHeaderComponent.extend({ + attributeBindings: ["names:data-name"], + classNames: "multi-select-header", + layoutName: "select-kit/templates/components/multi-select/multi-select-header", + selectedNameComponent: Ember.computed.alias("options.selectedNameComponent"), + + @on("didRender") + _positionFilter() { + if (this.get("shouldDisplayFilter") === false) { return; } + + const $filter = this.$(".filter"); + $filter.width(0); + + const leftHeaderOffset = this.$().offset().left; + const leftFilterOffset = $filter.offset().left; + const offset = leftFilterOffset - leftHeaderOffset; + const width = this.$().outerWidth(false); + const availableSpace = width - offset; + const $choices = $filter.parent(".choices"); + const parentRightPadding = parseInt($choices.css("padding-right") , 10); + $filter.width(availableSpace - parentRightPadding * 4); + }, + + @computed("computedContent.selectedComputedContents.[]") + names(selectedComputedContents) { + return Ember.makeArray(selectedComputedContents).map(sc => sc.name).join(","); + } +}); diff --git a/app/assets/javascripts/select-kit/components/multi-select/selected-category.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/selected-category.js.es6 new file mode 100644 index 0000000000..0a09d69fab --- /dev/null +++ b/app/assets/javascripts/select-kit/components/multi-select/selected-category.js.es6 @@ -0,0 +1,13 @@ +import SelectedNameComponent from "select-kit/components/multi-select/selected-name"; +import computed from "ember-addons/ember-computed-decorators"; +import { categoryBadgeHTML } from "discourse/helpers/category-link"; + +export default SelectedNameComponent.extend({ + classNames: "selected-category", + layoutName: "select-kit/templates/components/multi-select/selected-category", + + @computed("content.originalContent") + badge(category) { + return categoryBadgeHTML(category, {allowUncategorized: true, link: false}).htmlSafe(); + } +}); diff --git a/app/assets/javascripts/select-kit/components/multi-select/selected-color.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/selected-color.js.es6 new file mode 100644 index 0000000000..3349d95ff8 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/multi-select/selected-color.js.es6 @@ -0,0 +1,11 @@ +import SelectedNameComponent from "select-kit/components/multi-select/selected-name"; + +export default SelectedNameComponent.extend({ + classNames: "selected-color", + layoutName: "select-kit/templates/components/multi-select/selected-color", + + didRender() { + const name = this.get("content.name"); + this.$(".color-preview").css("background", `#${name}`.htmlSafe()); + } +}); diff --git a/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 new file mode 100644 index 0000000000..a201791c03 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 @@ -0,0 +1,28 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + attributeBindings: [ + "tabindex", + "content.name:data-name", + "content.value:data-value", + "guid:data-guid" + ], + classNames: ["selected-name", "choice"], + classNameBindings: ["isHighlighted", "isLocked"], + layoutName: "select-kit/templates/components/multi-select/selected-name", + tagName: "span", + tabindex: -1, + + @computed("content") + guid(content) { return Ember.guidFor(content); }, + + isLocked: Ember.computed("content.locked", function() { + return this.getWithDefault("content.locked", false); + }), + + click() { + if (this.get("isLocked") === true) { return false; } + this.toggleProperty("isHighlighted"); + return false; + } +}); diff --git a/app/assets/javascripts/select-kit/components/none-category-row.js.es6 b/app/assets/javascripts/select-kit/components/none-category-row.js.es6 new file mode 100644 index 0000000000..60b932920e --- /dev/null +++ b/app/assets/javascripts/select-kit/components/none-category-row.js.es6 @@ -0,0 +1,10 @@ +import CategoryRowComponent from "select-kit/components/category-row"; + +export default CategoryRowComponent.extend({ + layoutName: "select-kit/templates/components/category-row", + classNames: "none category-row", + + click() { + this.sendAction("onClear"); + } +}); diff --git a/app/assets/javascripts/select-box-kit/components/notifications-button.js.es6 b/app/assets/javascripts/select-kit/components/notifications-button.js.es6 similarity index 53% rename from app/assets/javascripts/select-box-kit/components/notifications-button.js.es6 rename to app/assets/javascripts/select-kit/components/notifications-button.js.es6 index 88a1e71e98..eaa9c6780f 100644 --- a/app/assets/javascripts/select-box-kit/components/notifications-button.js.es6 +++ b/app/assets/javascripts/select-kit/components/notifications-button.js.es6 @@ -1,4 +1,4 @@ -import DropdownSelectBoxComponent from "select-box-kit/components/dropdown-select-box"; +import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box"; import { default as computed, on } from "ember-addons/ember-computed-decorators"; import { buttonDetails } from "discourse/lib/notification-levels"; import { allLevels } from "discourse/lib/notification-levels"; @@ -9,28 +9,32 @@ export default DropdownSelectBoxComponent.extend({ fullWidthOnMobile: true, content: allLevels, collectionHeight: "auto", - value: Ember.computed.alias("notificationLevel"), castInteger: true, autofilterable: false, filterable: false, rowComponent: "notifications-button/notifications-button-row", - headerComponent: "notifications-button/notifications-button-header", - + allowInitialValueMutation: false, i18nPrefix: "", i18nPostfix: "", - showFullTitle: true, - @on("didReceiveAttrs", "didUpdateAttrs") - _setComponentOptions() { - this.set("headerComponentOptions", Ember.Object.create({ - i18nPrefix: this.get("i18nPrefix"), - showFullTitle: this.get("showFullTitle"), - })); + @computed("iconForSelectedDetails") + headerIcon(iconForSelectedDetails) { return iconForSelectedDetails; }, - this.set("rowComponentOptions", Ember.Object.create({ + iconForSelectedDetails: Ember.computed.alias("selectedDetails.icon"), + + computeHeaderContent() { + let content = this.baseHeaderComputedContent(); + content.name = I18n.t(`${this.get("i18nPrefix")}.${this.get("selectedDetails.key")}.title`); + content.hasSelection = this.get("hasSelection"); + return content; + }, + + @on("didReceiveAttrs") + _setNotificationsButtonComponentOptions() { + this.get("rowComponentOptions").setProperties({ i18nPrefix: this.get("i18nPrefix"), i18nPostfix: this.get("i18nPostfix") - })); + }); }, @computed("computedValue") diff --git a/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-row.js.es6 b/app/assets/javascripts/select-kit/components/notifications-button/notifications-button-row.js.es6 similarity index 76% rename from app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-row.js.es6 rename to app/assets/javascripts/select-kit/components/notifications-button/notifications-button-row.js.es6 index 6f3f257b43..e2e89d3a0b 100644 --- a/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/notifications-button/notifications-button-row.js.es6 @@ -1,4 +1,4 @@ -import DropdownSelectBoxRoxComponent from "select-box-kit/components/dropdown-select-box/dropdown-select-box-row"; +import DropdownSelectBoxRoxComponent from "select-kit/components/dropdown-select-box/dropdown-select-box-row"; import { buttonDetails } from "discourse/lib/notification-levels"; import computed from "ember-addons/ember-computed-decorators"; import { iconHTML } from 'discourse-common/lib/icon-library'; @@ -9,13 +9,13 @@ export default DropdownSelectBoxRoxComponent.extend({ i18nPrefix: Ember.computed.alias("options.i18nPrefix"), i18nPostfix: Ember.computed.alias("options.i18nPostfix"), - @computed("content.value", "i18nPrefix") + @computed("computedContent.value", "i18nPrefix") title(value, prefix) { const key = buttonDetails(value).key; return I18n.t(`${prefix}.${key}.title`); }, - @computed("content.name", "content.originalContent.icon") + @computed("computedContent.name", "computedContent.originalContent.icon") icon(contentName, icon) { return iconHTML(icon, { class: contentName.dasherize() }); }, @@ -30,7 +30,7 @@ export default DropdownSelectBoxRoxComponent.extend({ return Handlebars.escapeExpression(I18n.t(`${_start}.title`)); }, - @computed("i18nPrefix", "i18nPostfix", "content.name") + @computed("i18nPrefix", "i18nPostfix", "computedContent.name") _start(prefix, postfix, contentName) { return `${prefix}.${contentName}${postfix}`; }, diff --git a/app/assets/javascripts/select-box-kit/components/pinned-button.js.es6 b/app/assets/javascripts/select-kit/components/pinned-button.js.es6 similarity index 71% rename from app/assets/javascripts/select-box-kit/components/pinned-button.js.es6 rename to app/assets/javascripts/select-kit/components/pinned-button.js.es6 index c36d53c236..079274a2a8 100644 --- a/app/assets/javascripts/select-box-kit/components/pinned-button.js.es6 +++ b/app/assets/javascripts/select-kit/components/pinned-button.js.es6 @@ -1,12 +1,13 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ + pluginApiIdentifiers: ["pinned-button"], descriptionKey: "help", classNames: "pinned-button", classNameBindings: ["isHidden"], - layoutName: "select-box-kit/templates/components/pinned-button", + layoutName: "select-kit/templates/components/pinned-button", - @computed("topic.pinned_globally", "topic.pinned") + @computed("topic.pinned_globally", "pinned") reasonText(pinnedGlobally, pinned) { const globally = pinnedGlobally ? "_globally" : ""; const pinnedKey = pinned ? `pinned${globally}` : "unpinned"; @@ -14,7 +15,7 @@ export default Ember.Component.extend({ return I18n.t(key); }, - @computed("topic.pinned", "topic.deleted", "topic.unpinned") + @computed("pinned", "topic.deleted", "topic.unpinned") isHidden(pinned, deleted, unpinned) { return deleted || (!pinned && !unpinned); } diff --git a/app/assets/javascripts/select-kit/components/pinned-options.js.es6 b/app/assets/javascripts/select-kit/components/pinned-options.js.es6 new file mode 100644 index 0000000000..ebf5134432 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/pinned-options.js.es6 @@ -0,0 +1,55 @@ +import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box"; +import { on } from "ember-addons/ember-computed-decorators"; +import { iconHTML } from 'discourse-common/lib/icon-library'; + +export default DropdownSelectBoxComponent.extend({ + pluginApiIdentifiers: ["pinned-options"], + classNames: "pinned-options", + allowInitialValueMutation: false, + + autoHighlight() {}, + + computeHeaderContent() { + let content = this.baseHeaderComputedContent(); + const pinnedGlobally = this.get("topic.pinned_globally"); + const pinned = this.get("computedValue"); + const globally = pinnedGlobally ? "_globally" : ""; + const state = pinned ? `pinned${globally}` : "unpinned"; + const title = I18n.t(`topic_statuses.${state}.title`); + + content.name = `${title}${iconHTML("caret-down")}`.htmlSafe(); + content.dataName = title; + content.icon = `thumb-tack ${state === "unpinned" ? "unpinned" : null}`; + return content; + }, + + @on("init") + _setContent() { + const globally = this.get("topic.pinned_globally") ? "_globally" : ""; + + this.set("content", [ + { + id: "pinned", + name: I18n.t("topic_statuses.pinned" + globally + ".title"), + description: I18n.t('topic_statuses.pinned' + globally + '.help'), + icon: "thumb-tack" + }, + { + id: "unpinned", + name: I18n.t("topic_statuses.unpinned.title"), + icon: "thumb-tack unpinned", + description: I18n.t('topic_statuses.unpinned.help'), + } + ]); + }, + + mutateValue(value) { + const topic = this.get("topic"); + + if (value === "unpinned") { + topic.clearPin(); + } else { + topic.rePin(); + } + } +}); diff --git a/app/assets/javascripts/select-kit/components/search-advanced-category-chooser.js.es6 b/app/assets/javascripts/select-kit/components/search-advanced-category-chooser.js.es6 new file mode 100644 index 0000000000..7ed95af14a --- /dev/null +++ b/app/assets/javascripts/select-kit/components/search-advanced-category-chooser.js.es6 @@ -0,0 +1,29 @@ +import CategoryChooserComponent from "select-kit/components/category-chooser"; +import Category from "discourse/models/category"; + +export default CategoryChooserComponent.extend({ + pluginApiIdentifiers: ["search-advanced-category-chooser"], + classNames: ["search-advanced-category-chooser"], + rootNone: true, + rootNoneLabel: "category.all", + allowUncategorized: true, + clearable: true, + + init() { + this._super(); + + this.get("rowComponentOptions").setProperties({ + displayCategoryDescription: false + }); + }, + + mutateValue(value) { + if (value) { + this.set("value", Category.findById(value)); + } else { + this.set("value", null); + } + }, + + computeValue(category) { if (category) return category.id; } +}); diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6 new file mode 100644 index 0000000000..8f76dc045a --- /dev/null +++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6 @@ -0,0 +1,245 @@ +const { isNone } = Ember; +import computed from "ember-addons/ember-computed-decorators"; +import UtilsMixin from "select-kit/mixins/utils"; +import DomHelpersMixin from "select-kit/mixins/dom-helpers"; +import EventsMixin from "select-kit/mixins/events"; +import PluginApiMixin from "select-kit/mixins/plugin-api"; +import { + applyContentPluginApiCallbacks +} from "select-kit/mixins/plugin-api"; + +export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixin, EventsMixin, { + pluginApiIdentifiers: ["select-kit"], + layoutName: "select-kit/templates/components/select-kit", + classNames: ["select-kit", "select-box-kit"], + classNameBindings: [ + "isFocused", + "isExpanded", + "isDisabled", + "isHidden", + "isAbove", + "isBelow", + "isLeftAligned", + "isRightAligned" + ], + isDisabled: false, + isExpanded: false, + isFocused: false, + isHidden: false, + renderedBodyOnce: false, + renderedFilterOnce: false, + tabindex: 0, + scrollableParentSelector: ".modal-body", + none: null, + highlightedValue: null, + noContentLabel: "select_kit.no_content", + valueAttribute: "id", + nameProperty: "name", + autoFilterable: false, + filterable: false, + filter: "", + filterPlaceholder: "select_kit.filter_placeholder", + filterIcon: "search", + headerIcon: null, + rowComponent: "select-kit/select-kit-row", + rowComponentOptions: null, + noneRowComponent: "select-kit/select-kit-none-row", + createRowComponent: "select-kit/select-kit-create-row", + filterComponent: "select-kit/select-kit-filter", + headerComponent: "select-kit/select-kit-header", + headerComponentOptions: null, + headerComputedContent: null, + collectionComponent: "select-kit/select-kit-collection", + collectionHeight: 200, + verticalOffset: 0, + horizontalOffset: 0, + fullWidthOnMobile: false, + castInteger: false, + allowAny: false, + allowInitialValueMutation: false, + content: null, + computedContent: null, + limitMatches: 100, + nameChanges: false, + allowsContentReplacement: false, + + init() { + this._super(); + + this.noneValue = "__none__"; + this._previousScrollParentOverflow = "auto"; + this._previousCSSContext = {}; + this.set("headerComponentOptions", Ember.Object.create()); + this.set("rowComponentOptions", Ember.Object.create()); + this.set("computedContent", []); + + if (this.site && this.site.isMobileDevice) { + this.setProperties({ filterable: false, autoFilterable: false }); + } + + if (this.get("nameChanges")) { + this.addObserver(`content.@each.${this.get("nameProperty")}`, this, this._compute); + } + + if (this.get("allowsContentReplacement")) { + this.addObserver(`content.[]`, this, this._compute); + } + }, + + willDestroyElement() { + this.removeObserver(`content.@each.${this.get("nameProperty")}`, this, this._compute); + this.removeObserver(`content.[]`, this, this._compute); + }, + + willComputeAttributes() {}, + didComputeAttributes() {}, + + willComputeContent(content) { return content; }, + computeContent(content) { return content; }, + _beforeDidComputeContent(content) { + content = applyContentPluginApiCallbacks(this.get("pluginApiIdentifiers"), content, this); + + const existingCreatedComputedContent = this.get("computedContent").filterBy("created", true); + this.setProperties({ + computedContent: content.map(c => this.computeContentItem(c)).concat(existingCreatedComputedContent) + }); + return content; + }, + didComputeContent() {}, + + computeHeaderContent() { + return this.baseHeaderComputedContent(); + }, + + computeContentItem(contentItem, options) { + return this.baseComputedContentItem(contentItem, options); + }, + + baseComputedContentItem(contentItem, options) { + let originalContent; + options = options || {}; + const name = options.name; + + if (typeof contentItem === "string" || typeof contentItem === "number") { + originalContent = {}; + originalContent[this.get("valueAttribute")] = contentItem; + originalContent[this.get("nameProperty")] = name || contentItem; + } else { + originalContent = contentItem; + } + + return { + value: this._castInteger(this.valueForContentItem(contentItem)), + name: name || this._nameForContent(contentItem), + locked: false, + created: options.created || false, + originalContent + }; + }, + + @computed("shouldFilter", "allowAny", "filter") + shouldDisplayFilter(shouldFilter, allowAny, filter) { + if (shouldFilter === true) return true; + if (allowAny === true && filter.length > 0) return true; + return false; + }, + + @computed("filter", "filteredComputedContent.[]") + shouldDisplayNoContentRow(filter, filteredComputedContent) { + return filter.length > 0 && filteredComputedContent.length === 0; + }, + + @computed("filter", "filterable", "autoFilterable", "renderedFilterOnce") + shouldFilter(filter, filterable, autoFilterable, renderedFilterOnce) { + if (renderedFilterOnce === true && filterable === true) return true; + if (filterable === true) return true; + if (autoFilterable === true && filter.length > 0) return true; + return false; + }, + + @computed("filter", "computedContent") + shouldDisplayCreateRow(filter, computedContent) { + if (computedContent.map(c => c.value).includes(filter)) return false; + if (this.get("allowAny") === true && filter.length > 0) return true; + return false; + }, + + @computed("filter", "shouldDisplayCreateRow") + createRowComputedContent(filter, shouldDisplayCreateRow) { + if (shouldDisplayCreateRow === true) { + let content = this.createContentFromInput(filter); + return this.computeContentItem(content, { created: true }); + } + }, + + @computed + templateForRow() { return () => null; }, + + @computed + templateForNoneRow() { return () => null; }, + + @computed("filter") + templateForCreateRow() { + return (rowComponent) => { + return I18n.t("select_box.create", { + content: rowComponent.get("computedContent.name") + }); + }; + }, + + @computed("none") + noneRowComputedContent(none) { + if (isNone(none)) { return null; } + + switch (typeof none) { + case "string": + return this.computeContentItem(this.noneValue, { name: I18n.t(none) }); + default: + return this.computeContentItem(none); + } + }, + + createContentFromInput(input) { return input; }, + + willSelect() { + this.clearFilter(); + this.set("highlightedValue", null); + }, + didSelect() { + this.collapse(); + this.focus(); + }, + + willDeselect() { + this.clearFilter(); + this.set("highlightedValue", null); + }, + didDeselect() { + this.collapse(); + this.focus(); + }, + + clearFilter() { + this.$filterInput().val(""); + this.setProperties({ filter: "" }); + }, + + actions: { + onToggle() { + this.get("isExpanded") === true ? this.collapse() : this.expand(); + }, + + onHighlight(rowComputedContent) { + this.set("highlightedValue", rowComputedContent.value); + }, + + onFilter(filter) { + this.setProperties({ + highlightedValue: null, + renderedFilterOnce: true, + filter + }); + this.autoHighlight(); + } + } +}); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-collection.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-collection.js.es6 new file mode 100644 index 0000000000..67508609d2 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-collection.js.es6 @@ -0,0 +1,5 @@ +export default Ember.Component.extend({ + layoutName: "select-kit/templates/components/select-kit/select-kit-collection", + classNames: ["select-kit-collection", "select-box-kit-collection"], + tagName: "ul" +}); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-create-row.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-create-row.js.es6 new file mode 100644 index 0000000000..390408349b --- /dev/null +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-create-row.js.es6 @@ -0,0 +1,10 @@ +import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row"; + +export default SelectKitRowComponent.extend({ + layoutName: "select-kit/templates/components/select-kit/select-kit-row", + classNames: "create", + + click() { + this.sendAction("onCreate", this.get("computedContent")); + }, +}); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6 new file mode 100644 index 0000000000..f60a627de8 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6 @@ -0,0 +1,6 @@ +export default Ember.Component.extend({ + layoutName: "select-kit/templates/components/select-kit/select-kit-filter", + classNames: ["select-kit-filter", "select-box-kit-filter"], + classNameBindings: ["isFocused", "isHidden"], + isHidden: Ember.computed.not("shouldDisplayFilter") +}); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 new file mode 100644 index 0000000000..30863d179c --- /dev/null +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 @@ -0,0 +1,35 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + layoutName: "select-kit/templates/components/select-kit/select-kit-header", + classNames: ["select-kit-header", "select-box-kit-header"], + classNameBindings: ["isFocused"], + attributeBindings: [ + "dataName:data-name", + "tabindex", + "ariaLabel:aria-label", + "ariaHasPopup:aria-haspopup", + "title" + ], + + ariaHasPopup: true, + + ariaLabel: Ember.computed.alias("title"), + + name: Ember.computed.alias("computedContent.name"), + + @computed("computedContent.icon", "computedContent.icons") + icons(icon, icons) { + return Ember.makeArray(icon).concat(icons).filter(i => !Ember.isEmpty(i)); + }, + + @computed("computedContent.dataName", "name") + dataName(dataName, name) { return dataName || name; }, + + @computed("computedContent.title", "name") + title(title, name) { return title || name; }, + + click() { + this.sendAction("onToggle"); + } +}); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-none-row.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-none-row.js.es6 new file mode 100644 index 0000000000..542b9472c4 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-none-row.js.es6 @@ -0,0 +1,10 @@ +import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row"; + +export default SelectKitRowComponent.extend({ + layoutName: "select-kit/templates/components/select-kit/select-kit-row", + classNames: "none", + + click() { + this.sendAction("onClear"); + } +}); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 new file mode 100644 index 0000000000..f9c46b0a1e --- /dev/null +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 @@ -0,0 +1,58 @@ +import { on } from 'ember-addons/ember-computed-decorators'; +import computed from 'ember-addons/ember-computed-decorators'; +const { run, isPresent, makeArray, isEmpty } = Ember; +import UtilsMixin from "select-kit/mixins/utils"; + +export default Ember.Component.extend(UtilsMixin, { + layoutName: "select-kit/templates/components/select-kit/select-kit-row", + classNames: ["select-kit-row", "select-box-kit-row"], + tagName: "li", + tabIndex: -1, + attributeBindings: [ + "tabIndex", + "title", + "computedContent.value:data-value", + "computedContent.name:data-name" + ], + classNameBindings: ["isHighlighted", "isSelected"], + + @computed("computedContent.title", "computedContent.name") + title(title, name) { return title || name; }, + + @computed("templateForRow") + template(templateForRow) { return templateForRow(this); }, + + @on("didReceiveAttrs") + _setSelectionState() { + const contentValue = this.get("computedContent.value"); + + this.set("isSelected", this.get("computedValue") === contentValue); + this.set("isHighlighted", this.get("highlightedValue") === contentValue); + }, + + @on("willDestroyElement") + _clearDebounce() { + const hoverDebounce = this.get("hoverDebounce"); + if (isPresent(hoverDebounce)) { run.cancel(hoverDebounce); } + }, + + @computed("computedContent.icon", "computedContent.icons", "computedContent.originalContent.icon") + icons(icon, icons, originalIcon) { + return makeArray(icon) + .concat(icons) + .concat(makeArray(originalIcon)) + .filter(i => !isEmpty(i)); + }, + + mouseEnter() { + this.set("hoverDebounce", run.debounce(this, this._sendOnHighlightAction, 32)); + }, + + click() { + this.sendAction("onSelect", this.get("computedContent")); + }, + + _sendOnHighlightAction() { + this.sendAction("onHighlight", this.get("computedContent")); + } +}); diff --git a/app/assets/javascripts/select-kit/components/single-select.js.es6 b/app/assets/javascripts/select-kit/components/single-select.js.es6 new file mode 100644 index 0000000000..5f539a5268 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/single-select.js.es6 @@ -0,0 +1,172 @@ +import SelectKitComponent from "select-kit/components/select-kit"; +import { on } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; +const { get, isNone, isEmpty, isPresent, run } = Ember; +import { + applyOnSelectPluginApiCallbacks +} from "select-kit/mixins/plugin-api"; + +export default SelectKitComponent.extend({ + pluginApiIdentifiers: ["single-select"], + classNames: "single-select", + computedValue: null, + value: null, + allowInitialValueMutation: false, + + @on("didReceiveAttrs") + _compute() { + run.scheduleOnce("afterRender", () => { + this.willComputeAttributes(); + let content = this.willComputeContent(this.get("content") || []); + let value = this._beforeWillComputeValue(this.get("value")); + content = this.computeContent(content); + content = this._beforeDidComputeContent(content); + value = this.willComputeValue(value); + value = this.computeValue(value); + value = this._beforeDidComputeValue(value); + this.didComputeContent(content); + this.didComputeValue(value); + this.set("headerComputedContent", this.computeHeaderContent()); + this.didComputeAttributes(); + + if (this.get("allowInitialValueMutation")) this.mutateAttributes(); + }); + }, + + mutateAttributes() { + if (this.get("isDestroyed") || this.get("isDestroying")) return; + + run.next(() => { + this.mutateContent(this.get("computedContent")); + this.mutateValue(this.get("computedValue")); + applyOnSelectPluginApiCallbacks(this.get("pluginApiIdentifiers"), this.get("computedValue"), this); + this.set("headerComputedContent", this.computeHeaderContent()); + }); + }, + mutateContent() {}, + mutateValue(computedValue) { + this.set("value", computedValue); + }, + + _beforeWillComputeValue(value) { + if (!isEmpty(this.get("content")) && isEmpty(value) && isNone(this.get("none"))) { + value = this.valueForContentItem(get(this.get("content"), "firstObject")); + } + + switch (typeof value) { + case "string": + case "number": + return this._castInteger(value === "" ? null : value); + default: + return value; + } + }, + willComputeValue(value) { return value; }, + computeValue(value) { return value; }, + _beforeDidComputeValue(value) { + this.setProperties({ computedValue: value }); + return value; + }, + didComputeValue(value) { return value; }, + + filterComputedContent(computedContent, computedValue, filter) { + const lowerFilter = filter.toLowerCase(); + return computedContent.filter(c => { + return get(c, "name").toLowerCase().indexOf(lowerFilter) > -1; + }); + }, + + baseHeaderComputedContent() { + return { + icons: Ember.makeArray(this.getWithDefault("headerIcon", [])), + name: this.get("selectedComputedContent.name") || this.get("noneRowComputedContent.name") + }; + }, + + @computed("computedContent.[]", "computedValue", "filter", "shouldFilter") + filteredComputedContent(computedContent, computedValue, filter, shouldFilter) { + if (shouldFilter === true) { + computedContent = this.filterComputedContent(computedContent, computedValue, filter); + } + + return computedContent.slice(0, this.get("limitMatches")); + }, + + @computed("computedValue", "computedContent.[]") + selectedComputedContent(computedValue, computedContent) { + if (isNone(computedValue) || isNone(computedContent)) { return null; } + return computedContent.findBy("value", computedValue); + }, + + @computed("selectedComputedContent") + hasSelection(selectedComputedContent) { + return selectedComputedContent !== this.get("noneRowComputedContent") && + !Ember.isNone(selectedComputedContent); + }, + + @computed("filter", "computedValue") + shouldDisplayCreateRow(filter, computedValue) { + return this._super() && computedValue !== filter; + }, + + autoHighlight() { + run.schedule("afterRender", () => { + if (!isNone(this.get("highlightedValue"))) { return; } + + const filteredComputedContent = this.get("filteredComputedContent"); + const displayCreateRow = this.get("shouldDisplayCreateRow"); + const none = this.get("noneRowComputedContent"); + + if (this.get("hasSelection") && isEmpty(this.get("filter"))) { + this.send("onHighlight", this.get("selectedComputedContent")); + return; + } + + if (isNone(this.get("highlightedValue")) && !isEmpty(filteredComputedContent)) { + this.send("onHighlight", get(filteredComputedContent, "firstObject")); + return; + } + + if (displayCreateRow === true && isEmpty(filteredComputedContent)) { + this.send("onHighlight", this.get("createRowComputedContent")); + } + else if (!isEmpty(filteredComputedContent)) { + this.send("onHighlight", get(filteredComputedContent, "firstObject")); + } + else if (isEmpty(filteredComputedContent) && isPresent(none) && displayCreateRow === false) { + this.send("onHighlight", none); + } + }); + }, + + validateComputedContentItem(computedContentItem) { + return this.get("computedValue") !== computedContentItem.value; + }, + + actions: { + onClear() { + this.send("onDeselect", this.get("selectedComputedContent")); + }, + + onCreate(computedContentItem) { + if (this.validateComputedContentItem(computedContentItem)) { + this.get("computedContent").pushObject(computedContentItem); + this.send("onSelect", computedContentItem); + } + }, + + onSelect(rowComputedContentItem) { + this.willSelect(rowComputedContentItem); + this.set("computedValue", rowComputedContentItem.value); + this.mutateAttributes(); + run.schedule("afterRender", () => this.didSelect(rowComputedContentItem)); + }, + + onDeselect(rowComputedContentItem) { + this.willDeselect(rowComputedContentItem); + this.set("computedValue", null); + this.mutateAttributes(); + run.schedule("afterRender", () => this.didDeselect(rowComputedContentItem)); + } + } +}); diff --git a/app/assets/javascripts/select-kit/components/tag-notifications-button.js.es6 b/app/assets/javascripts/select-kit/components/tag-notifications-button.js.es6 new file mode 100644 index 0000000000..7208f8c81a --- /dev/null +++ b/app/assets/javascripts/select-kit/components/tag-notifications-button.js.es6 @@ -0,0 +1,23 @@ +import NotificationOptionsComponent from "select-kit/components/notifications-button"; +import computed from "ember-addons/ember-computed-decorators"; + +export default NotificationOptionsComponent.extend({ + pluginApiIdentifiers: ["tag-notifications-button"], + classNames: "tag-notifications-button", + i18nPrefix: "tagging.notifications", + showFullTitle: false, + allowInitialValueMutation: false, + + mutateValue(value) { + this.sendAction("action", value); + }, + + computeValue() { + return this.get("notificationLevel"); + }, + + @computed("iconForSelectedDetails") + headerIcon(iconForSelectedDetails) { + return [iconForSelectedDetails, "caret-down"]; + } +}); diff --git a/app/assets/javascripts/select-kit/components/topic-footer-mobile-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/topic-footer-mobile-dropdown.js.es6 new file mode 100644 index 0000000000..ccdf3af489 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/topic-footer-mobile-dropdown.js.es6 @@ -0,0 +1,68 @@ +import ComboBoxComponent from "select-kit/components/combo-box"; + +export default ComboBoxComponent.extend({ + pluginApiIdentifiers: ["topic-footer-mobile-dropdown"], + classNames: "topic-footer-mobile-dropdown", + filterable: false, + autoFilterable: false, + allowInitialValueMutation: false, + + computeHeaderContent() { + let content = this.baseHeaderComputedContent(); + content.name = I18n.t("topic.controls"); + return content; + }, + + computeContent(content) { + const topic = this.get("topic"); + const details = topic.get("details"); + + if (details.get("can_invite_to")) { + content.push({ id: "invite", icon: "users", name: I18n.t("topic.invite_reply.title") }); + } + + if (topic.get("bookmarked")) { + content.push({ id: "bookmark", icon: "bookmark", name: I18n.t("bookmarked.clear_bookmarks") }); + } else { + content.push({ id: "bookmark", icon: "bookmark", name: I18n.t("bookmarked.title") }); + } + + content.push({ id: "share", icon: "link", name: I18n.t("topic.share.title") }); + + if (details.get("can_flag_topic")) { + content.push({ id: "flag", icon: "flag", name: I18n.t("topic.flag_topic.title") }); + } + + return content; + }, + + autoHighlight() {}, + + mutateValue(value) { + const topic = this.get("topic"); + + if (!topic.get("id")) { + return; + } + + const refresh = () => this.send("onDeselect", value); + + switch(value) { + case "invite": + this.attrs.showInvite(); + refresh(); + break; + case "bookmark": + topic.toggleBookmark().then(() => refresh() ); + break; + case "share": + this.appEvents.trigger("share:url", topic.get("shareUrl"), $("#topic-footer-buttons")); + refresh(); + break; + case "flag": + this.attrs.showFlagTopic(); + refresh(); + break; + } + } +}); diff --git a/app/assets/javascripts/select-box-kit/components/topic-notifications-button.js.es6 b/app/assets/javascripts/select-kit/components/topic-notifications-button.js.es6 similarity index 62% rename from app/assets/javascripts/select-box-kit/components/topic-notifications-button.js.es6 rename to app/assets/javascripts/select-kit/components/topic-notifications-button.js.es6 index 3aeb9a9d98..741b89106f 100644 --- a/app/assets/javascripts/select-box-kit/components/topic-notifications-button.js.es6 +++ b/app/assets/javascripts/select-kit/components/topic-notifications-button.js.es6 @@ -1,5 +1,5 @@ export default Ember.Component.extend({ - layoutName: "select-box-kit/templates/components/topic-notifications-button", + layoutName: "select-kit/templates/components/topic-notifications-button", classNames: "topic-notifications-button", showFullTitle: true, appendReason: true diff --git a/app/assets/javascripts/select-box-kit/components/topic-notifications-options.js.es6 b/app/assets/javascripts/select-kit/components/topic-notifications-options.js.es6 similarity index 65% rename from app/assets/javascripts/select-box-kit/components/topic-notifications-options.js.es6 rename to app/assets/javascripts/select-kit/components/topic-notifications-options.js.es6 index d1da27684a..4feb3b003f 100644 --- a/app/assets/javascripts/select-box-kit/components/topic-notifications-options.js.es6 +++ b/app/assets/javascripts/select-kit/components/topic-notifications-options.js.es6 @@ -1,12 +1,13 @@ -import NotificationOptionsComponent from "select-box-kit/components/notifications-button"; +import NotificationOptionsComponent from "select-kit/components/notifications-button"; import { on } from "ember-addons/ember-computed-decorators"; import { topicLevels } from "discourse/lib/notification-levels"; export default NotificationOptionsComponent.extend({ + pluginApiIdentifiers: ["topic-notifications-options"], classNames: "topic-notifications-options", content: topicLevels, i18nPrefix: "topic.notifications", - value: Ember.computed.alias("topic.details.notification_level"), + allowInitialValueMutation: false, @on("didInsertElement") _bindGlobalLevelChanged() { @@ -24,17 +25,9 @@ export default NotificationOptionsComponent.extend({ this.appEvents.off("topic-notifications-button:changed"); }, - actions: { - onSelect(value) { - if (value !== this.get("computedValue")) { - this.get("topic.details").updateNotifications(value); - } - - this.set("value", value); - - this.defaultOnSelect(value); - - this.blur(); + mutateValue(value) { + if (value !== this.get("value")) { + this.get("topic.details").updateNotifications(value); } } }); diff --git a/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 b/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 new file mode 100644 index 0000000000..eddd2bfb21 --- /dev/null +++ b/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 @@ -0,0 +1,233 @@ +import { on } from "ember-addons/ember-computed-decorators"; + +export default Ember.Mixin.create({ + init() { + this._super(); + + this.filterInputSelector = ".filter-input"; + this.rowSelector = ".select-kit-row"; + this.collectionSelector = ".select-kit-collection"; + this.headerSelector = ".select-kit-header"; + this.bodySelector = ".select-kit-body"; + this.wrapperSelector = ".select-kit-wrapper"; + }, + + $findRowByValue(value) { return this.$(`${this.rowSelector}[data-value='${value}']`); }, + + $header() { return this.$(this.headerSelector); }, + + $body() { return this.$(this.bodySelector); }, + + $collection() { return this.$(this.collectionSelector); }, + + $rows(withHidden) { + + if (withHidden === true) { + return this.$(`${this.rowSelector}:not(.no-content)`); + } else { + return this.$(`${this.rowSelector}:not(.no-content):not(.is-hidden)`); + } + }, + + $highlightedRow() { return this.$rows().filter(".is-highlighted"); }, + + $selectedRow() { return this.$rows().filter(".is-selected"); }, + + $filterInput() { return this.$(this.filterInputSelector); }, + + @on("didRender") + _ajustPosition() { + $(`.select-kit-fixed-placeholder-${this.elementId}`).remove(); + this.$collection().css("max-height", this.get("collectionHeight")); + this._applyFixedPosition(); + this._applyDirection(); + this._positionWrapper(); + }, + + @on("willDestroyElement") + _clearState() { + $(`.select-kit-fixed-placeholder-${this.elementId}`).remove(); + }, + + // use to collapse and remove focus + close(event) { + this.collapse(event); + this.setProperties({ isFocused: false }); + }, + + // force the component in a known default state + focus() { + Ember.run.schedule("afterRender", () => this.$header().focus()); + }, + + // try to focus filter and fallback to header if not present + focusFilterOrHeader() { + Ember.run.schedule("afterRender", () => { + if ((this.site && this.site.isMobileDevice) || !this.$filterInput().is(":visible")) { + this.$header().focus(); + } else { + this.$filterInput().focus(); + } + }); + }, + + expand() { + if (this.get("isExpanded") === true) return; + this.setProperties({ isExpanded: true, renderedBodyOnce: true, isFocused: true }); + this.focusFilterOrHeader(); + this.autoHighlight(); + }, + + collapse() { + this.set("isExpanded", false); + Ember.run.schedule("afterRender", () => this._removeFixedPosition() ); + }, + + // lose focus of the component in two steps + // first collapse and keep focus and then remove focus + unfocus(event) { + if (this.get("isExpanded") === true) { + this.collapse(event); + this.focus(event); + } else { + this.close(event); + } + }, + + _destroyEvent(event) { + event.preventDefault(); + event.stopPropagation(); + }, + + _applyDirection() { + let options = { left: "auto", bottom: "auto", top: "auto" }; + + const dHeader = $(".d-header")[0]; + const dHeaderBounds = dHeader ? dHeader.getBoundingClientRect() : {top: 0, height: 0}; + const dHeaderHeight = dHeaderBounds.top + dHeaderBounds.height; + const bodyHeight = this.$body()[0].getBoundingClientRect().height; + const windowWidth = $(window).width(); + const windowHeight = $(window).height(); + const boundingRect = this.get("element").getBoundingClientRect(); + const componentHeight = boundingRect.height; + const componentWidth = boundingRect.width; + const offsetTop = boundingRect.top; + const offsetBottom = boundingRect.bottom; + + if (this.get("fullWidthOnMobile") && (this.site && this.site.isMobileDevice)) { + const margin = 10; + const relativeLeft = this.$().offset().left - $(window).scrollLeft(); + options.left = margin - relativeLeft; + options.width = windowWidth - margin * 2; + options.maxWidth = options.minWidth = "unset"; + } else { + const bodyWidth = this.$body()[0].getBoundingClientRect().width; + + if ($("html").css("direction") === "rtl") { + const horizontalSpacing = boundingRect.right; + const hasHorizontalSpace = horizontalSpacing - (this.get("horizontalOffset") + bodyWidth) > 0; + if (hasHorizontalSpace) { + this.setProperties({ isLeftAligned: true, isRightAligned: false }); + options.left = bodyWidth + this.get("horizontalOffset"); + } else { + this.setProperties({ isLeftAligned: false, isRightAligned: true }); + options.right = - (bodyWidth - componentWidth + this.get("horizontalOffset")); + } + } else { + const horizontalSpacing = boundingRect.left; + const hasHorizontalSpace = (windowWidth - (this.get("horizontalOffset") + horizontalSpacing + bodyWidth) > 0); + if (hasHorizontalSpace) { + this.setProperties({ isLeftAligned: true, isRightAligned: false }); + options.left = this.get("horizontalOffset"); + } else { + this.setProperties({ isLeftAligned: false, isRightAligned: true }); + options.right = this.get("horizontalOffset"); + } + } + } + + const fullHeight = this.get("verticalOffset") + bodyHeight + componentHeight; + const hasBelowSpace = windowHeight - offsetBottom - fullHeight > 0; + const hasAboveSpace = offsetTop - fullHeight - dHeaderHeight > 0; + if (hasBelowSpace || (!hasBelowSpace && !hasAboveSpace)) { + this.setProperties({ isBelow: true, isAbove: false }); + options.top = this.$header()[0].getBoundingClientRect().height + this.get("verticalOffset"); + } else { + this.setProperties({ isBelow: false, isAbove: true }); + options.bottom = this.$header()[0].getBoundingClientRect().height + this.get("verticalOffset"); + } + + this.$body().css(options); + }, + + _applyFixedPosition() { + if (this.get("isExpanded") !== true) { return; } + + const scrollableParent = this.$().parents(this.get("scrollableParentSelector")); + if (scrollableParent.length === 0) { return; } + + const boundingRect = this.get("element").getBoundingClientRect(); + const width = boundingRect.width; + const height = boundingRect.height; + const $placeholder = $(`
    `); + + this._previousScrollParentOverflow = this._previousScrollParentOverflow || scrollableParent.css("overflow"); + scrollableParent.css({ overflow: "hidden" }); + + this._previousCSSContext = { + minWidth: this.$().css("min-width"), + maxWidth: this.$().css("max-width") + }; + + const componentStyles = { + position: "fixed", + "margin-top": -scrollableParent.scrollTop(), + width, + minWidth: "unset", + maxWidth: "unset" + }; + + if ($("html").css("direction") === "rtl") { + componentStyles.marginRight = -width; + } else { + componentStyles.marginLeft = -width; + } + + $placeholder.css({ display: "inline-block", width, height, "vertical-align": "middle" }); + + this.$().before($placeholder).css(componentStyles); + }, + + _removeFixedPosition() { + $(`.select-kit-fixed-placeholder-${this.elementId}`).remove(); + + if (!this.element || this.isDestroying || this.isDestroyed) { return; } + + const scrollableParent = this.$().parents(this.get("scrollableParentSelector")); + if (scrollableParent.length === 0) { return; } + + const css = jQuery.extend( + this._previousCSSContext, + { + top: "auto", + left: "auto", + "margin-left": "auto", + "margin-right": "auto", + "margin-top": "auto", + position: "relative" + } + ); + this.$().css(css); + + scrollableParent.css("overflow", this._previousScrollParentOverflow); + }, + + _positionWrapper() { + const headerBoundingRect = this.$header()[0].getBoundingClientRect(); + + this.$(this.wrapperSelector).css({ + width: this.get("element").getBoundingClientRect().width, + height: headerBoundingRect.height + this.$body()[0].getBoundingClientRect().height + }); + }, +}); diff --git a/app/assets/javascripts/select-kit/mixins/events.js.es6 b/app/assets/javascripts/select-kit/mixins/events.js.es6 new file mode 100644 index 0000000000..a033f63b4a --- /dev/null +++ b/app/assets/javascripts/select-kit/mixins/events.js.es6 @@ -0,0 +1,232 @@ +export default Ember.Mixin.create({ + init() { + this._super(); + + this.keys = { + TAB: 9, + ENTER: 13, + ESC: 27, + UP: 38, + DOWN: 40, + BACKSPACE: 8, + }; + }, + + willDestroyElement() { + this._super(); + + $(document).off("mousedown.select-kit"); + + if (this.$header()) { + this.$header() + .off("blur.select-kit") + .off("focus.select-kit") + .off("keypress.select-kit") + .off("keydown.select-kit"); + } + + if (this.$filterInput()) { + this.$filterInput() + .off("change.select-kit") + .off("keydown.select-kit") + .off("keypress.select-kit"); + } + }, + + didInsertElement() { + this._super(); + + $(document) + .on("mousedown.select-kit", event => { + if (Ember.isNone(this.get("element"))) return; + if (this.get("element").contains(event.target)) return; + + this.didClickOutside(event); + }); + + this.$header() + .on("blur.select-kit", () => { + if (this.get("isExpanded") === false && this.get("isFocused") === true) { + this.close(); + } + }) + .on("focus.select-kit", (event) => { + this.set("isFocused", true); + this._destroyEvent(event); + }) + .on("keydown.select-kit", (event) => { + const keyCode = event.keyCode || event.which; + + if (document.activeElement !== this.$header()[0]) return event; + + if (keyCode === this.keys.TAB) this.tabFromHeader(event); + if (keyCode === this.keys.BACKSPACE) this.backspaceFromHeader(event); + if (keyCode === this.keys.ESC) this.escapeFromHeader(event); + if (keyCode === this.keys.ENTER) this.enterFromHeader(event); + if ([this.keys.UP, this.keys.DOWN].includes(keyCode)) this.upAndDownFromHeader(event); + return event; + }) + .on("keypress.select-kit", (event) => { + const keyCode = event.keyCode || event.which; + + if (keyCode === this.keys.ENTER) { return true; } + if (keyCode === this.keys.TAB) { return true; } + + this.expand(event); + + if (this.get("filterable") === true || this.get("autoFilterable")) { + this.set("renderedFilterOnce", true); + } + + if (keyCode >= 65 && keyCode <= 122) { + Ember.run.schedule("afterRender", () => { + this.$filterInput() + .focus() + .val(this.$filterInput().val() + String.fromCharCode(keyCode)); + }); + } + + return false; + }); + + this.$filterInput() + .on("change.select-kit", (event) => { + this.send("onFilter", $(event.target).val()); + }) + .on("keypress.select-kit", (event) => { + event.stopPropagation(); + }) + .on("keydown.select-kit", (event) => { + const keyCode = event.keyCode || event.which; + + if (keyCode === this.keys.TAB) this.tabFromFilter(event); + if (keyCode === this.keys.ESC) this.escapeFromFilter(event); + if (keyCode === this.keys.ENTER) this.enterFromFilter(event); + if ([this.keys.UP, this.keys.DOWN].includes(keyCode)) this.upAndDownFromFilter(event); + }); + }, + + didPressTab(event) { + if (this.get("isExpanded") === false) { + this.unfocus(event); + } else if (this.$highlightedRow().length === 1) { + Ember.run.throttle(this, this._rowClick, this.$highlightedRow(), 150, 150, true); + this.unfocus(event); + return true; + } else { + this._destroyEvent(event); + this.unfocus(event); + } + + return true; + }, + + didPressEscape(event) { + this._destroyEvent(event); + this.unfocus(event); + }, + + didPressUpAndDownArrows(event) { + this._destroyEvent(event); + + const keyCode = event.keyCode || event.which; + const $rows = this.$rows(); + + if (this.get("isExpanded") === false) { + this.expand(event); + + if (this.$selectedRow().length === 1) { + this._highlightRow(this.$selectedRow()); + return; + } + } + + if ($rows.length <= 0) { return; } + if ($rows.length === 1) { + this._rowSelection($rows, 0); + return; + } + + const direction = keyCode === 38 ? -1 : 1; + + Ember.run.throttle(this, this._moveHighlight, direction, $rows, 32); + }, + + didPressBackspace(event) { + this._destroyEvent(event); + + this.expand(event); + + if (this.$filterInput().is(":visible")) { + this.$filterInput().focus().trigger(event).trigger("change"); + } + }, + + didPressEnter(event) { + this._destroyEvent(event); + + if (this.get("isExpanded") === false) { + this.expand(event); + } else if (this.$highlightedRow().length === 1) { + Ember.run.throttle(this, this._rowClick, this.$highlightedRow(), 150, true); + } + }, + + didClickOutside(event) { + if ($(event.target).parents(".select-kit").length === 1) { + this.close(event); + return false; + } + + this.close(event); + return; + }, + + // make sure we don’t propagate a click outside component + // to avoid closing a modal containing the component for example + click(event) { + this._destroyEvent(event); + }, + + tabFromHeader(event) { this.didPressTab(event); }, + tabFromFilter(event) { this.didPressTab(event); }, + + escapeFromHeader(event) { this.didPressEscape(event); }, + escapeFromFilter(event) { this.didPressEscape(event); }, + + upAndDownFromHeader(event) { this.didPressUpAndDownArrows(event); }, + upAndDownFromFilter(event) { this.didPressUpAndDownArrows(event); }, + + backspaceFromHeader(event) { this.didPressBackspace(event); }, + + enterFromHeader(event) { this.didPressEnter(event); }, + enterFromFilter(event) { this.didPressEnter(event); }, + + _moveHighlight(direction, $rows) { + const currentIndex = $rows.index(this.$highlightedRow()); + let nextIndex = currentIndex + direction; + + if (nextIndex < 0) { + nextIndex = $rows.length - 1; + } else if (nextIndex >= $rows.length) { + nextIndex = 0; + } + + this._rowSelection($rows, nextIndex); + }, + + _rowClick($row) { $row.click(); }, + + _rowSelection($rows, nextIndex) { + const highlightableValue = $rows.eq(nextIndex).attr("data-value"); + const $highlightableRow = this.$findRowByValue(highlightableValue); + this._highlightRow($highlightableRow); + }, + + _highlightRow($row) { + Ember.run.schedule("afterRender", () => { + $row.trigger("mouseover").focus(); + this.focus(); + }); + } +}); diff --git a/app/assets/javascripts/select-kit/mixins/plugin-api.js.es6 b/app/assets/javascripts/select-kit/mixins/plugin-api.js.es6 new file mode 100644 index 0000000000..5fd32fcd1d --- /dev/null +++ b/app/assets/javascripts/select-kit/mixins/plugin-api.js.es6 @@ -0,0 +1,91 @@ +let _appendContentCallbacks = {}; +function appendContent(pluginApiIdentifiers, contentFunction) { + if (Ember.isNone(_appendContentCallbacks[pluginApiIdentifiers])) { + _appendContentCallbacks[pluginApiIdentifiers] = []; + } + + _appendContentCallbacks[pluginApiIdentifiers].push(contentFunction); +} + +let _prependContentCallbacks = {}; +function prependContent(pluginApiIdentifiers, contentFunction) { + if (Ember.isNone(_prependContentCallbacks[pluginApiIdentifiers])) { + _prependContentCallbacks[pluginApiIdentifiers] = []; + } + + _prependContentCallbacks[pluginApiIdentifiers].push(contentFunction); +} + +let _modifyContentCallbacks = {}; +function modifyContent(pluginApiIdentifiers, contentFunction) { + if (Ember.isNone(_modifyContentCallbacks[pluginApiIdentifiers])) { + _modifyContentCallbacks[pluginApiIdentifiers] = []; + } + + _modifyContentCallbacks[pluginApiIdentifiers].push(contentFunction); +} + +let _onSelectCallbacks = {}; +function onSelect(pluginApiIdentifiers, mutationFunction) { + if (Ember.isNone(_onSelectCallbacks[pluginApiIdentifiers])) { + _onSelectCallbacks[pluginApiIdentifiers] = []; + } + + _onSelectCallbacks[pluginApiIdentifiers].push(mutationFunction); +} + +export function applyContentPluginApiCallbacks(identifiers, content, context) { + identifiers.forEach((key) => { + (_prependContentCallbacks[key] || []).forEach((c) => { + content = c().concat(content); + }); + (_appendContentCallbacks[key] || []).forEach((c) => { + content = content.concat(c()); + }); + (_modifyContentCallbacks[key] || []).forEach((c) => { + content = c(context, content); + }); + }); + + return content; +} + +export function applyOnSelectPluginApiCallbacks(identifiers, val, context) { + identifiers.forEach((key) => { + (_onSelectCallbacks[key] || []).forEach((c) => c(context, val)); + }); +} + +export function modifySelectKit(pluginApiIdentifiers) { + return { + appendContent: (content) => { + appendContent(pluginApiIdentifiers, () => {return content;} ); + return modifySelectKit(pluginApiIdentifiers); + }, + prependContent: (content) => { + prependContent(pluginApiIdentifiers, () => {return content;} ); + return modifySelectKit(pluginApiIdentifiers); + }, + modifyContent: (callback) => { + modifyContent(pluginApiIdentifiers, callback); + return modifySelectKit(pluginApiIdentifiers); + }, + onSelect: (callback) => { + onSelect(pluginApiIdentifiers, callback); + return modifySelectKit(pluginApiIdentifiers); + } + }; +} + +export function clearCallbacks() { + _appendContentCallbacks = {}; + _prependContentCallbacks = {}; + _modifyContentCallbacks = {}; + _onSelectCallbacks = {}; +} + +const EMPTY_ARRAY = Object.freeze([]); +export default Ember.Mixin.create({ + concatenatedProperties: ["pluginApiIdentifiers"], + pluginApiIdentifiers: EMPTY_ARRAY +}); diff --git a/app/assets/javascripts/select-kit/mixins/utils.js.es6 b/app/assets/javascripts/select-kit/mixins/utils.js.es6 new file mode 100644 index 0000000000..54f404b3b9 --- /dev/null +++ b/app/assets/javascripts/select-kit/mixins/utils.js.es6 @@ -0,0 +1,47 @@ +const { get, isNone, guidFor } = Ember; + +export default Ember.Mixin.create({ + valueForContentItem(content) { + switch (typeof content) { + case "string": + case "number": + return content; + default: + return get(content, this.get("valueAttribute")); + } + }, + + _nameForContent(content) { + if (isNone(content)) { + return null; + } + + if (typeof content === "object") { + return get(content, this.get("nameProperty")); + } + + return content; + }, + + _isNumeric(input) { + return !isNaN(parseFloat(input)) && isFinite(input); + }, + + _castInteger(value) { + if (this.get("castInteger") && Ember.isPresent(value) && this._isNumeric(value)) { + return parseInt(value, 10); + } + + return value; + }, + + _findComputedContentItemByGuid(guid) { + return this.get("computedContent").find(c => { + return guidFor(c) === guid; + }); + }, + + _filterRemovableComputedContents(computedContent) { + return computedContent.filter(c => c.created === true); + } +}); diff --git a/app/assets/javascripts/select-kit/templates/components/category-row.hbs b/app/assets/javascripts/select-kit/templates/components/category-row.hbs new file mode 100644 index 0000000000..0f3249ea46 --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/category-row.hbs @@ -0,0 +1,19 @@ +{{#if category}} + {{#if hasParentCategory}} +
    + {{badgeForParentCategory}} {{badgeForCategory}} +  × {{topicCount}} +
    + {{else}} +
    + {{badgeForCategory}} +  × {{topicCount}} +
    + {{/if}} + + {{#if shouldDisplayDescription}} +
    {{{description}}}
    + {{/if}} +{{else}} + {{computedContent.name}} +{{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs new file mode 100644 index 0000000000..adc9440ce5 --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs @@ -0,0 +1,13 @@ +{{#each icons as |icon|}} {{d-icon icon}} {{/each}} + + + {{{name}}} + + +{{#if shouldDisplayClearableButton}} + +{{/if}} + +{{d-icon caretIcon class="caret-icon"}} diff --git a/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs new file mode 100644 index 0000000000..9cc8c31cf6 --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs @@ -0,0 +1,7 @@ +{{#each icons as |icon|}} {{d-icon icon}} {{/each}} + +{{#if options.showFullTitle}} + + {{name}} + +{{/if}} diff --git a/app/assets/javascripts/select-box-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs similarity index 78% rename from app/assets/javascripts/select-box-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs rename to app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs index 0fe1813c8a..556551a61f 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs +++ b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-row.hbs @@ -1,10 +1,10 @@ {{#if template}} {{{template}}} {{else}} - {{#if icon}} + {{#if icons}}
    - {{{icon}}} + {{#each icons as |icon|}} {{d-icon icon}} {{/each}}
    {{/if}} diff --git a/app/assets/javascripts/select-box-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs similarity index 51% rename from app/assets/javascripts/select-box-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs rename to app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs index 5e175c5ee7..4c8904e9a3 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs @@ -1,21 +1,21 @@ -{{#if icon}} +{{#if icons}}
    - {{{icon}}} + {{#each icons as |icon|}} {{d-icon icon}} {{/each}}
    {{/if}} - - {{{selectedName}}} + + {{{name}}} -{{#if datetime}} +{{#if computedContent.datetime}} - {{datetime}} + {{computedContent.datetime}} {{/if}} {{#if shouldDisplayClearableButton}} - {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs new file mode 100644 index 0000000000..8d62c2d1f9 --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-row.hbs @@ -0,0 +1,13 @@ +{{#if computedContent.icons}} +
    + {{#each computedContent.icons as |icon|}} {{d-icon icon}} {{/each}} +
    +{{/if}} + +{{computedContent.name}} + +{{#if computedContent.datetime}} + + {{computedContent.datetime}} + +{{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs new file mode 100644 index 0000000000..466b5c6d25 --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs @@ -0,0 +1,13 @@ +
    + {{#each computedContent.selectedComputedContents as |selectedComputedContent|}} + {{component selectedNameComponent onDeselect=onDeselect content=selectedComputedContent}} + {{/each}} + + {{component "select-kit/select-kit-filter" + onFilter=onFilter + shouldDisplayFilter=shouldDisplayFilter + isFocused=isFocused + filter=filter + }} + +
    diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-category.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-category.hbs new file mode 100644 index 0000000000..c220d68cfc --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-category.hbs @@ -0,0 +1,7 @@ + + + {{d-icon "times"}} + + + {{badge}} + diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-color.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-color.hbs new file mode 100644 index 0000000000..c0d544ab9d --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-color.hbs @@ -0,0 +1,13 @@ +
    + + {{#unless isLocked}} + + {{d-icon "times"}} + + {{/unless}} + + #{{content.name}} + + + +
    diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs new file mode 100644 index 0000000000..721a30ef21 --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs @@ -0,0 +1,13 @@ +{{#if isLocked}} + + {{d-icon "lock"}} + +{{else}} + + {{d-icon "times"}} + +{{/if}} + + + {{content.name}} + diff --git a/app/assets/javascripts/select-box-kit/templates/components/pinned-button.hbs b/app/assets/javascripts/select-kit/templates/components/pinned-button.hbs similarity index 50% rename from app/assets/javascripts/select-box-kit/templates/components/pinned-button.hbs rename to app/assets/javascripts/select-kit/templates/components/pinned-button.hbs index aaabc2f1bf..31fc07f1dc 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/pinned-button.hbs +++ b/app/assets/javascripts/select-kit/templates/components/pinned-button.hbs @@ -1,4 +1,4 @@ -{{pinned-options topic=topic}} +{{pinned-options value=pinned topic=topic}}

    {{{reasonText}}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit.hbs new file mode 100644 index 0000000000..d15ba708af --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/select-kit.hbs @@ -0,0 +1,49 @@ +{{component headerComponent + tabindex=tabindex + isFocused=isFocused + isExpanded=isExpanded + computedContent=headerComputedContent + onDeselect=(action "onDeselect") + onToggle=(action "onToggle") + onFilter=(action "onFilter") + onClear=(action "onClear") + options=headerComponentOptions + shouldDisplayFilter=shouldDisplayFilter +}} + +

    + {{component filterComponent + onFilter=(action "onFilter") + icon=filterIcon + shouldDisplayFilter=shouldDisplayFilter + isFocused=isFocused + placeholder=(i18n filterPlaceholder) + filter=filter + }} + + {{#if renderedBodyOnce}} + {{component collectionComponent + hasSelection=hasSelection + noneRowComputedContent=noneRowComputedContent + createRowComputedContent=createRowComputedContent + filteredComputedContent=filteredComputedContent + rowComponent=rowComponent + noneRowComponent=noneRowComponent + createRowComponent=createRowComponent + templateForRow=templateForRow + templateForNoneRow=templateForNoneRow + templateForCreateRow=templateForCreateRow + onClear=(action "onClear") + onSelect=(action "onSelect") + onHighlight=(action "onHighlight") + onCreate=(action "onCreate") + noContentLabel=noContentLabel + highlightedValue=highlightedValue + computedValue=computedValue + shouldDisplayNoContentRow=shouldDisplayNoContentRow + rowComponentOptions=rowComponentOptions + }} + {{/if}} +
    + +
    diff --git a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-collection.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs similarity index 58% rename from app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-collection.hbs rename to app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs index 73f80e2608..824f34f2c1 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-collection.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs @@ -1,12 +1,10 @@ -{{#if none}} - {{#if selectedContent}} +{{#if hasSelection}} + {{#if noneRowComputedContent}} {{component noneRowComponent - content=none + computedContent=noneRowComputedContent templateForRow=templateForNoneRow - titleForRow=titleForRow - iconForRow=iconForRow highlightedValue=highlightedValue - onClearSelection=onClearSelection + onClear=onClear onHighlight=onHighlight value=computedValue options=rowComponentOptions @@ -14,36 +12,34 @@ {{/if}} {{/if}} -{{#if shouldDisplayCreateRow}} +{{#if createRowComputedContent}} {{component createRowComponent - content=createRowContent + computedContent=createRowComputedContent templateForRow=templateForCreateRow - titleForRow=titleForRow - iconForRow=iconForRow highlightedValue=highlightedValue onHighlight=onHighlight - onCreateContent=onCreateContent + onCreate=onCreate value=computedValue options=rowComponentOptions }} {{/if}} -{{#each filteredContent as |content|}} +{{#each filteredComputedContent as |computedContent|}} {{component rowComponent - content=content + computedContent=computedContent templateForRow=templateForRow - titleForRow=titleForRow - iconForRow=iconForRow highlightedValue=highlightedValue onSelect=onSelect onHighlight=onHighlight - value=computedValue + computedValue=computedValue options=rowComponentOptions }} -{{else}} +{{/each}} + +{{#if shouldDisplayNoContentRow}} {{#if noContentLabel}} -
  • +
  • {{i18n noContentLabel}}
  • {{/if}} -{{/each}} +{{/if}} diff --git a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-filter.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-filter.hbs similarity index 69% rename from app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-filter.hbs rename to app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-filter.hbs index 74d1288074..dd62fca94a 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-filter.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-filter.hbs @@ -1,8 +1,8 @@ {{input - tabindex=tabindex - class="select-box-kit-filter-input" + tabindex=-1 + class="filter-input" placeholder=placeholder - key-down=onFilterChange + key-up=onFilter autocomplete="off" autocorrect="off" autocapitalize="off" diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs new file mode 100644 index 0000000000..abd119afda --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs @@ -0,0 +1,5 @@ +{{#each icons as |icon|}} {{d-icon icon}} {{/each}} + + + {{{name}}} + diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs new file mode 100644 index 0000000000..80444173f4 --- /dev/null +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs @@ -0,0 +1,6 @@ +{{#if template}} + {{{template}}} +{{else}} + {{#each icons as |icon|}} {{d-icon icon}} {{/each}} + {{computedContent.name}} +{{/if}} diff --git a/app/assets/javascripts/select-box-kit/templates/components/topic-notifications-button.hbs b/app/assets/javascripts/select-kit/templates/components/topic-notifications-button.hbs similarity index 50% rename from app/assets/javascripts/select-box-kit/templates/components/topic-notifications-button.hbs rename to app/assets/javascripts/select-kit/templates/components/topic-notifications-button.hbs index 17d9953055..2569ea0c86 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/topic-notifications-button.hbs +++ b/app/assets/javascripts/select-kit/templates/components/topic-notifications-button.hbs @@ -1,4 +1,7 @@ -{{topic-notifications-options topic=topic showFullTitle=showFullTitle}} +{{topic-notifications-options + value=notificationLevel + topic=topic + showFullTitle=showFullTitle}} {{#if appendReason}}

    diff --git a/app/assets/javascripts/service-worker.js.erb b/app/assets/javascripts/service-worker.js.erb new file mode 100644 index 0000000000..43fb85a33e --- /dev/null +++ b/app/assets/javascripts/service-worker.js.erb @@ -0,0 +1,92 @@ +'use strict'; + +// Incrementing CACHE_VERSION will kick off the install event and force previously cached +// resources to be cached again. +const CACHE_VERSION = 1; + +const CURRENT_CACHES = { + offline: 'offline-v' + CACHE_VERSION +}; + +const OFFLINE_URL = 'offline.html'; + +function createCacheBustedRequest(url) { + var request = new Request(url, {cache: 'reload'}); + // See https://fetch.spec.whatwg.org/#concept-request-mode + // This is not yet supported in Chrome as of M48, so we need to explicitly check to see + // if the cache: 'reload' option had any effect. + if ('cache' in request) { + return request; + } + + // If {cache: 'reload'} didn't have any effect, append a cache-busting URL parameter instead. + var bustedUrl = new URL(url, self.location.href); + bustedUrl.search += (bustedUrl.search ? '&' : '') + 'cachebust=' + Date.now(); + return new Request(bustedUrl); +} + +self.addEventListener('install', function(event) { + event.waitUntil( + // We can't use cache.add() here, since we want OFFLINE_URL to be the cache key, but + // the actual URL we end up requesting might include a cache-busting parameter. + fetch(createCacheBustedRequest(OFFLINE_URL)).then(function(response) { + return caches.open(CURRENT_CACHES.offline).then(function(cache) { + return cache.put(OFFLINE_URL, response); + }); + }) + ); +}); + +self.addEventListener('activate', function(event) { + // Delete all caches that aren't named in CURRENT_CACHES. + // While there is only one cache in this example, the same logic will handle the case where + // there are multiple versioned caches. + var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) { + return CURRENT_CACHES[key]; + }); + + event.waitUntil( + caches.keys().then(function(cacheNames) { + return Promise.all( + cacheNames.map(function(cacheName) { + if (expectedCacheNames.indexOf(cacheName) === -1) { + // If this cache name isn't present in the array of "expected" cache names, + // then delete it. + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); + +self.addEventListener('fetch', function(event) { + // We only want to call event.respondWith() if this is a navigation request + // for an HTML page. + // request.mode of 'navigate' is unfortunately not supported in Chrome + // versions older than 49, so we need to include a less precise fallback, + // which checks for a GET request with an Accept: text/html header. + if (event.request.mode === 'navigate' || + (event.request.method === 'GET' && + event.request.headers.get('accept').includes('text/html'))) { + event.respondWith( + fetch(event.request).catch(function(error) { + // The catch is only triggered if fetch() throws an exception, which will most likely + // happen due to the server being unreachable. + // If fetch() returns a valid HTTP response with an response code in the 4xx or 5xx + // range, the catch() will NOT be called. If you need custom handling for 4xx or 5xx + // errors, see https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker/fallback-response + return caches.match(OFFLINE_URL); + }) + ); + } + + // If our if() condition is false, then this fetch handler won't intercept the request. + // If there are any other fetch handlers registered, they will get a chance to call + // event.respondWith(). If no fetch handlers call event.respondWith(), the request will be + // handled by the browser as if there were no service worker involvement. +}); + +<% DiscoursePluginRegistry.service_workers.each do |js| %> +<%=raw "#{File.read(js)}" %> +<% end %> diff --git a/app/assets/javascripts/wizard-application.js b/app/assets/javascripts/wizard-application.js index 9baf2c224b..ef61997351 100644 --- a/app/assets/javascripts/wizard-application.js +++ b/app/assets/javascripts/wizard-application.js @@ -3,7 +3,7 @@ //= require ./ember-addons/macro-alias //= require ./ember-addons/ember-computed-decorators //= require_tree ./discourse-common -//= require_tree ./select-box-kit +//= require_tree ./select-kit //= require wizard/router //= require wizard/wizard //= require_tree ./wizard/templates diff --git a/app/assets/javascripts/wizard/templates/components/invite-list.hbs b/app/assets/javascripts/wizard/templates/components/invite-list.hbs index 7fec1dcfa1..c966c8bc00 100644 --- a/app/assets/javascripts/wizard/templates/components/invite-list.hbs +++ b/app/assets/javascripts/wizard/templates/components/invite-list.hbs @@ -12,7 +12,7 @@ {{input class="invite-email wizard-focusable" value=inviteEmail placeholder="user@example.com" tabindex="9"}}

    - {{combo-box value=inviteRole content=roles nameProperty="label"}} + {{combo-box allowInitialValueMutation=true value=inviteRole content=roles nameProperty="label"}}