diff --git a/.eslintrc b/.eslintrc index 4d652d710a..844933e68b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -48,6 +48,7 @@ "selectKitSelectRowByValue":true, "selectKitSelectRowByName":true, "selectKitSelectRowByIndex":true, + "keyboardHelper":true, "selectKitSelectNoneRow":true, "selectKitFillInFilter":true, "asyncTestDiscourse":true, diff --git a/.travis.yml b/.travis.yml index d905c1e37a..4a9baa5500 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,9 @@ addons: matrix: fast_finish: true + exclude: + - rvm: 2.4.4 + env: "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=1" rvm: - 2.5.1 diff --git a/Dangerfile b/Dangerfile index f418ed5b2c..3941cffbed 100644 --- a/Dangerfile +++ b/Dangerfile @@ -5,7 +5,7 @@ end prettier_offenses = `prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split('\n') if !prettier_offenses.empty? fail(%{ -This PR has multiple prettier offenses. Using prettier\n +This PR doesn't match our required code formatting standards, as enforced by prettier.io. Here's how to set up prettier in your code editor.\n #{prettier_offenses.map { |o| github.html_link(o) }.join("\n")} }) end diff --git a/Gemfile b/Gemfile index 3ec355c0a8..b71cbfab20 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.55' +gem 'onebox', '1.8.57' gem 'http_accept_language', '~>2.0.5', require: false @@ -88,6 +88,7 @@ gem 'thor', require: false gem 'rinku' gem 'sanitize' gem 'sidekiq' +gem 'mini_scheduler' # for sidekiq web gem 'tilt', require: false @@ -180,6 +181,8 @@ gem 'rqrcode' gem 'sshkey', require: false +gem 'rchardet', require: false + if ENV["IMPORT"] == "1" gem 'mysql2' gem 'redcarpet' diff --git a/Gemfile.lock b/Gemfile.lock index 2118599739..521090e794 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -200,6 +200,7 @@ GEM mini_portile2 (2.3.0) mini_racer (0.2.0) libv8 (>= 6.3) + mini_scheduler (0.8.1) mini_sql (0.1.10) mini_suffix (0.3.0) ffi (~> 1.9) @@ -256,7 +257,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.55) + onebox (1.8.57) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -320,6 +321,7 @@ GEM ffi (>= 1.0.6) msgpack (>= 0.4.3) trollop (>= 1.16.2) + rchardet (1.8.0) redis (4.0.1) redis-namespace (1.6.0) redis (>= 3.0.4) @@ -489,6 +491,7 @@ DEPENDENCIES message_bus mini_mime mini_racer + mini_scheduler mini_sql mini_suffix minitest @@ -506,7 +509,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.55) + onebox (= 1.8.57) openid-redis-store pg pry-nav @@ -521,6 +524,7 @@ DEPENDENCIES rb-fsevent rb-inotify (~> 0.9) rbtrace + rchardet redis redis-namespace rinku @@ -550,4 +554,4 @@ DEPENDENCIES webpush BUNDLED WITH - 1.16.2 + 1.16.3 diff --git a/app/assets/javascripts/admin/components/admin-report-counters.js.es6 b/app/assets/javascripts/admin/components/admin-report-counters.js.es6 new file mode 100644 index 0000000000..3806e29ecb --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-counters.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + classNames: ["admin-report-counters"] +}); diff --git a/app/assets/javascripts/admin/components/admin-report-table-cell.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-cell.js.es6 new file mode 100644 index 0000000000..7140b69668 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-table-cell.js.es6 @@ -0,0 +1,18 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + tagName: "td", + classNames: ["admin-report-table-cell"], + classNameBindings: ["type", "property"], + options: null, + + @computed("label", "data", "options") + computedLabel(label, data, options) { + return label.compute(data, options || {}); + }, + + type: Ember.computed.alias("label.type"), + property: Ember.computed.alias("label.mainProperty"), + formatedValue: Ember.computed.alias("computedLabel.formatedValue"), + value: Ember.computed.alias("computedLabel.value") +}); diff --git a/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 index b7c569cc25..ab986f2946 100644 --- a/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 @@ -3,10 +3,10 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ tagName: "th", classNames: ["admin-report-table-header"], - classNameBindings: ["label.property", "isCurrentSort"], + classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"], attributeBindings: ["label.title:title"], - @computed("currentSortLabel.sort_property", "label.sort_property") + @computed("currentSortLabel.sortProperty", "label.sortProperty") isCurrentSort(currentSortField, labelSortField) { return currentSortField === labelSortField; }, diff --git a/app/assets/javascripts/admin/components/admin-report-table-row.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-row.js.es6 index 3c4de4970e..3be140c308 100644 --- a/app/assets/javascripts/admin/components/admin-report-table-row.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table-row.js.es6 @@ -1,13 +1,5 @@ -import computed from "ember-addons/ember-computed-decorators"; - export default Ember.Component.extend({ tagName: "tr", classNames: ["admin-report-table-row"], - - @computed("data", "labels") - cells(row, labels) { - return labels.map(label => { - return label.compute(row); - }); - } + options: null }); diff --git a/app/assets/javascripts/admin/components/admin-report-table.js.es6 b/app/assets/javascripts/admin/components/admin-report-table.js.es6 index b39afc8a89..dc007a79cc 100644 --- a/app/assets/javascripts/admin/components/admin-report-table.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-table.js.es6 @@ -1,6 +1,5 @@ import computed from "ember-addons/ember-computed-decorators"; import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip"; -import { isNumeric } from "discourse/lib/utilities"; const PAGES_LIMIT = 8; @@ -67,14 +66,16 @@ export default Ember.Component.extend({ const computedLabel = label.compute(row); const value = computedLabel.value; - if (computedLabel.type === "link" || (value && !isNumeric(value))) { - return undefined; + if (!["seconds", "number", "percent"].includes(label.type)) { + return; } else { - return sum + value; + return sum + Math.round(value || 0); } }; - totalsRow[label.property] = rows.reduce(reducer, 0); + const total = rows.reduce(reducer, 0); + totalsRow[label.mainProperty] = + label.type === "percent" ? Math.round(total / rows.length) : total; }); return totalsRow; diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6 index 67d5b57378..fa57c3d821 100644 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report.js.es6 @@ -2,14 +2,15 @@ import Category from "discourse/models/category"; import { exportEntity } from "discourse/lib/export-csv"; import { outputExportResult } from "discourse/lib/export-result"; import { ajax } from "discourse/lib/ajax"; -import Report from "admin/models/report"; +import { SCHEMA_VERSION, default as Report } from "admin/models/report"; import computed from "ember-addons/ember-computed-decorators"; import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip"; const TABLE_OPTIONS = { perPage: 8, total: true, - limit: 20 + limit: 20, + formatNumbers: true }; const CHART_OPTIONS = {}; @@ -50,9 +51,10 @@ export default Ember.Component.extend({ reportOptions: null, forcedModes: null, showAllReportsLink: false, + filters: null, startDate: null, endDate: null, - categoryId: null, + category: null, groupId: null, showTrend: false, showHeader: true, @@ -77,7 +79,7 @@ export default Ember.Component.extend({ didReceiveAttrs() { this._super(...arguments); - const state = this.get("filteringState") || {}; + const state = this.get("filters") || {}; this.setProperties({ category: Category.findById(state.categoryId), groupId: state.groupId, @@ -109,7 +111,9 @@ export default Ember.Component.extend({ unregisterTooltip($(".info[data-tooltip]")); }, - showTimeoutError: Ember.computed.alias("model.timeout"), + showError: Ember.computed.or("showTimeoutError", "showExceptionError"), + showTimeoutError: Ember.computed.equal("model.error", "timeout"), + showExceptionError: Ember.computed.equal("model.error", "exception"), hasData: Ember.computed.notEmpty("model.data"), @@ -129,6 +133,8 @@ export default Ember.Component.extend({ return displayedModesLength > 1; }, + categoryId: Ember.computed.alias("category.id"), + @computed("currentMode", "model.modes", "forcedModes") displayedModes(currentMode, reportModes, forcedModes) { const modes = forcedModes ? forcedModes.split(",") : reportModes; @@ -186,24 +192,20 @@ export default Ember.Component.extend({ reportKey(dataSourceName, categoryId, groupId, startDate, endDate) { if (!dataSourceName || !startDate || !endDate) return null; - let reportKey = `reports:${dataSourceName}`; - - if (categoryId && categoryId !== "all") { - reportKey += `:${categoryId}`; - } else { - reportKey += `:`; - } - - reportKey += `:${startDate.replace(/-/g, "")}`; - reportKey += `:${endDate.replace(/-/g, "")}`; - - if (groupId && groupId !== "all") { - reportKey += `:${groupId}`; - } else { - reportKey += `:`; - } - - reportKey += `:`; + let reportKey = "reports:"; + reportKey += [ + dataSourceName, + categoryId, + startDate.replace(/-/g, ""), + endDate.replace(/-/g, ""), + groupId, + "[:prev_period]", + this.get("reportOptions.table.limit"), + SCHEMA_VERSION + ] + .filter(x => x) + .map(x => x.toString()) + .join(":"); return reportKey; }, @@ -211,7 +213,7 @@ export default Ember.Component.extend({ actions: { refreshReport() { this.attrs.onRefresh({ - categoryId: this.get("category.id"), + categoryId: this.get("categoryId"), groupId: this.get("groupId"), startDate: this.get("startDate"), endDate: this.get("endDate") @@ -346,12 +348,12 @@ export default Ember.Component.extend({ if (mode === "table") { const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS)); return Ember.Object.create( - _.assign(tableOptions, this.get("reportOptions.table") || {}) + Object.assign(tableOptions, this.get("reportOptions.table") || {}) ); } else { const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS)); return Ember.Object.create( - _.assign(chartOptions, this.get("reportOptions.chart") || {}) + Object.assign(chartOptions, this.get("reportOptions.chart") || {}) ); } }, diff --git a/app/assets/javascripts/admin/components/value-list.js.es6 b/app/assets/javascripts/admin/components/value-list.js.es6 index 60e4a3cda7..9e656bed70 100644 --- a/app/assets/javascripts/admin/components/value-list.js.es6 +++ b/app/assets/javascripts/admin/components/value-list.js.es6 @@ -1,113 +1,95 @@ +import { on } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; + export default Ember.Component.extend({ classNameBindings: [":value-list"], - _enableSorting: function() { - const self = this; - const placeholder = document.createElement("div"); - placeholder.className = "placeholder"; + inputInvalid: Ember.computed.empty("newValue"), - let dragging = null; - let over = null; - let nodePlacement; + inputDelimiter: null, + inputType: null, + newValue: "", + collection: null, + values: null, + noneKey: Ember.computed.alias("addKey"), - this.$().on("dragstart.discourse", ".values .value", function(e) { - dragging = e.currentTarget; - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/html", e.currentTarget); - }); - - this.$().on("dragend.discourse", ".values .value", function() { - Ember.run(function() { - dragging.parentNode.removeChild(placeholder); - dragging.style.display = "block"; - - // Update data - const from = Number(dragging.dataset.index); - let to = Number(over.dataset.index); - if (from < to) to--; - if (nodePlacement === "after") to++; - - const collection = self.get("collection"); - const fromObj = collection.objectAt(from); - collection.replace(from, 1); - collection.replace(to, 0, [fromObj]); - self._saveValues(); - }); - return false; - }); - - this.$().on("dragover.discourse", ".values", function(e) { - e.preventDefault(); - dragging.style.display = "none"; - if (e.target.className === "placeholder") { - return; - } - over = e.target; - - const relY = e.originalEvent.clientY - over.offsetTop; - const height = over.offsetHeight / 2; - const parent = e.target.parentNode; - - if (relY > height) { - nodePlacement = "after"; - parent.insertBefore(placeholder, e.target.nextElementSibling); - } else if (relY < height) { - nodePlacement = "before"; - parent.insertBefore(placeholder, e.target); - } - }); - }.on("didInsertElement"), - - _removeSorting: function() { - this.$() - .off("dragover.discourse") - .off("dragend.discourse") - .off("dragstart.discourse"); - }.on("willDestroyElement"), - - _setupCollection: function() { + @on("didReceiveAttrs") + _setupCollection() { const values = this.get("values"); if (this.get("inputType") === "array") { this.set("collection", values || []); - } else { - this.set("collection", values && values.length ? values.split("\n") : []); + return; } - } - .on("init") - .observes("values"), - _saveValues: function() { - if (this.get("inputType") === "array") { - this.set("values", this.get("collection")); - } else { - this.set("values", this.get("collection").join("\n")); - } + this.set( + "collection", + this._splitValues(values, this.get("inputDelimiter") || "\n") + ); }, - inputInvalid: Ember.computed.empty("newValue"), + @computed("choices.[]", "collection.[]") + filteredChoices(choices, collection) { + return Ember.makeArray(choices).filter(i => collection.indexOf(i) < 0); + }, - keyDown(e) { - if (e.keyCode === 13) { - this.send("addValue"); - } + keyDown(event) { + if (event.keyCode === 13) this.send("addValue", this.get("newValue")); }, actions: { - addValue() { - if (this.get("inputInvalid")) { - return; - } + changeValue(index, newValue) { + this._replaceValue(index, newValue); + }, + + addValue(newValue) { + if (this.get("inputInvalid")) return; - this.get("collection").addObject(this.get("newValue")); this.set("newValue", ""); - - this._saveValues(); + this._addValue(newValue); }, removeValue(value) { - const collection = this.get("collection"); - collection.removeObject(value); - this._saveValues(); + this._removeValue(value); + }, + + selectChoice(choice) { + this._addValue(choice); + } + }, + + _addValue(value) { + this.get("collection").addObject(value); + this._saveValues(); + }, + + _removeValue(value) { + const collection = this.get("collection"); + collection.removeObject(value); + this._saveValues(); + }, + + _replaceValue(index, newValue) { + this.get("collection").replace(index, 1, [newValue]); + this._saveValues(); + }, + + _saveValues() { + if (this.get("inputType") === "array") { + this.set("values", this.get("collection")); + return; + } + + this.set( + "values", + this.get("collection").join(this.get("inputDelimiter") || "\n") + ); + }, + + _splitValues(values, delimiter) { + if (values && values.length) { + return values.split(delimiter).filter(x => x); + } else { + return []; } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 index 42bdcc7824..7f81972a16 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-general.js.es6 @@ -4,18 +4,11 @@ import AdminDashboardNext from "admin/models/admin-dashboard-next"; import Report from "admin/models/report"; import PeriodComputationMixin from "admin/mixins/period-computation"; -const ACTIVITY_METRICS_REPORTS = [ - "page_view_total_reqs", - "visits", - "time_to_first_response", - "likes", - "flags", - "user_to_user_private_messages_with_replies" -]; - function staticReport(reportType) { return function() { - return this.get("reports").find(x => x.type === reportType); + return Ember.makeArray(this.get("reports")).find( + report => report.type === reportType + ); }.property("reports.[]"); } @@ -28,13 +21,25 @@ export default Ember.Controller.extend(PeriodComputationMixin, { lastBackupTakenAt: Ember.computed.alias( "model.attributes.last_backup_taken_at" ), - shouldDisplayDurability: Ember.computed.and("lastBackupTakenAt", "diskSpace"), + shouldDisplayDurability: Ember.computed.and("diskSpace"), @computed topReferredTopicsTopions() { return { table: { total: false, limit: 8 } }; }, + @computed + activityMetrics() { + return [ + "page_view_total_reqs", + "visits", + "time_to_first_response", + "likes", + "flags", + "user_to_user_private_messages_with_replies" + ]; + }, + @computed trendingSearchOptions() { return { table: { total: false, limit: 8 } }; @@ -43,13 +48,6 @@ export default Ember.Controller.extend(PeriodComputationMixin, { usersByTypeReport: staticReport("users_by_type"), usersByTrustLevelReport: staticReport("users_by_trust_level"), - @computed("reports.[]") - activityMetricsReports(reports) { - return reports.filter(report => - ACTIVITY_METRICS_REPORTS.includes(report.type) - ); - }, - fetchDashboard() { if (this.get("isLoading")) return; @@ -66,7 +64,9 @@ export default Ember.Controller.extend(PeriodComputationMixin, { this.setProperties({ dashboardFetchedAt: new Date(), model: adminDashboardNextModel, - reports: adminDashboardNextModel.reports.map(x => Report.create(x)) + reports: Ember.makeArray(adminDashboardNextModel.reports).map(x => + Report.create(x) + ) }); }) .catch(e => { @@ -77,17 +77,26 @@ export default Ember.Controller.extend(PeriodComputationMixin, { } }, + @computed("startDate", "endDate") + filters(startDate, endDate) { + return { startDate, endDate }; + }, + @computed("model.attributes.updated_at") updatedTimestamp(updatedAt) { - return moment(updatedAt).format("LLL"); + return moment(updatedAt) + .tz(moment.tz.guess()) + .format("LLL"); }, @computed("lastBackupTakenAt") backupTimestamp(lastBackupTakenAt) { - return moment(lastBackupTakenAt).format("LLL"); + return moment(lastBackupTakenAt) + .tz(moment.tz.guess()) + .format("LLL"); }, _reportsForPeriodURL(period) { - return Discourse.getURL(`/admin/dashboard/general?period=${period}`); + return Discourse.getURL(`/admin?period=${period}`); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 index 0958bc9d62..059bcd6176 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 @@ -2,8 +2,6 @@ import computed from "ember-addons/ember-computed-decorators"; import PeriodComputationMixin from "admin/mixins/period-computation"; export default Ember.Controller.extend(PeriodComputationMixin, { - exceptionController: Ember.inject.controller("exception"), - @computed flagsStatusOptions() { return { @@ -14,6 +12,16 @@ export default Ember.Controller.extend(PeriodComputationMixin, { }; }, + @computed("startDate", "endDate") + filters(startDate, endDate) { + return { startDate, endDate }; + }, + + @computed("lastWeek", "endDate") + lastWeekfilters(startDate, endDate) { + return { startDate, endDate }; + }, + _reportsForPeriodURL(period) { return Discourse.getURL(`/admin/dashboard/moderation?period=${period}`); } diff --git a/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 index fdabfd896e..e04b16f6a7 100644 --- a/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 @@ -5,7 +5,7 @@ export default Ember.Controller.extend({ @computed("model.type") reportOptions(type) { - let options = { table: { perPage: 50, limit: 50 } }; + let options = { table: { perPage: 50, limit: 50, formatNumbers: false } }; if (type === "top_referred_topics") { options.table.limit = 10; @@ -15,7 +15,7 @@ export default Ember.Controller.extend({ }, @computed("category_id", "group_id", "start_date", "end_date") - filteringState(categoryId, groupId, startDate, endDate) { + filters(categoryId, groupId, startDate, endDate) { return { categoryId, groupId, diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6 index e00f14f4b3..9636c47732 100644 --- a/app/assets/javascripts/admin/mixins/setting-component.js.es6 +++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6 @@ -10,7 +10,8 @@ const CUSTOM_TYPES = [ "category_list", "value_list", "category", - "uploaded_image_list" + "uploaded_image_list", + "compact_list" ]; export default Ember.Mixin.create({ @@ -59,11 +60,20 @@ export default Ember.Mixin.create({ return setting.replace(/\_/g, " "); }, - @computed("setting.type") + @computed("type") componentType(type) { return CUSTOM_TYPES.indexOf(type) !== -1 ? type : "string"; }, + @computed("setting") + type(setting) { + if (setting.type === "list" && setting.list_type) { + return `${setting.list_type}_list`; + } + + return setting.type; + }, + @computed("typeClass") componentName(typeClass) { return "site-settings/" + typeClass; diff --git a/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 index 5ad85c0399..6898f8191a 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 @@ -6,7 +6,7 @@ const AdminDashboardNext = Discourse.Model.extend({}); AdminDashboardNext.reopenClass({ fetch() { - return ajax("/admin/dashboard-next.json").then(json => { + return ajax("/admin/dashboard.json").then(json => { const model = AdminDashboardNext.create(); model.set("version_check", json.version_check); return model; diff --git a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 index 75757656d6..9ce5a79e15 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 @@ -11,7 +11,7 @@ AdminDashboard.reopenClass({ @return {jqXHR} a jQuery Promise object **/ find: function() { - return ajax("/admin/dashboard.json").then(function(json) { + return ajax("/admin/dashboard-old.json").then(function(json) { var model = AdminDashboard.create(json); model.set("loaded", true); return model; diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 39a2b7ee1d..ec230ffff4 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -530,11 +530,14 @@ const AdminUser = Discourse.User.extend({ } }, - @computed("suspended_by") suspendedBy: wrapAdmin, + @computed("suspended_by") + suspendedBy: wrapAdmin, - @computed("silenced_by") silencedBy: wrapAdmin, + @computed("silenced_by") + silencedBy: wrapAdmin, - @computed("approved_by") approvedBy: wrapAdmin + @computed("approved_by") + approvedBy: wrapAdmin }); AdminUser.reopenClass({ diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 566aad0170..fc4fb8e86a 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -1,87 +1,20 @@ import { escapeExpression } from "discourse/lib/utilities"; import { ajax } from "discourse/lib/ajax"; import round from "discourse/lib/round"; -import { fillMissingDates, isNumeric } from "discourse/lib/utilities"; +import { fillMissingDates, formatUsername } from "discourse/lib/utilities"; import computed from "ember-addons/ember-computed-decorators"; import { number, durationTiny } from "discourse/lib/formatter"; +import { renderAvatar } from "discourse/helpers/user-avatar"; + +// Change this line each time report format change +// and you want to ensure cache is reset +export const SCHEMA_VERSION = 2; const Report = Discourse.Model.extend({ average: false, percent: false, higher_is_better: true, - @computed("labels") - computedLabels(labels) { - return labels.map(label => { - const type = label.type; - const properties = label.properties; - const property = properties[0]; - - return { - title: label.title, - sort_property: label.sort_property || property, - property, - compute: row => { - let value = row[property]; - let escapedValue = escapeExpression(value); - let tooltip; - let base = { property, value, type }; - - if (value === null || typeof value === "undefined") { - return _.assign(base, { - value: null, - formatedValue: "-", - type: "undefined" - }); - } - - if (type === "seconds") { - return _.assign(base, { - formatedValue: escapeExpression(durationTiny(value)) - }); - } - - if (type === "link") { - return _.assign(base, { - formatedValue: `${escapedValue}` - }); - } - - if (type === "percent") { - return _.assign(base, { - formatedValue: `${escapedValue}%` - }); - } - - if (type === "number" || isNumeric(value)) - return _.assign(base, { - type: "number", - formatedValue: number(value) - }); - - if (type === "date") { - const date = moment(value, "YYYY-MM-DD"); - if (date.isValid()) { - return _.assign(base, { - formatedValue: date.format("LL") - }); - } - } - - if (type === "text") tooltip = escapedValue; - - return _.assign(base, { - tooltip, - type: type || "string", - formatedValue: escapedValue - }); - } - }; - }); - }, - @computed("modes") onlyTable(modes) { return modes.length === 1 && modes[0] === "table"; @@ -312,6 +245,169 @@ const Report = Discourse.Model.extend({ return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/); }, + @computed("labels") + computedLabels(labels) { + return labels.map(label => { + const type = label.type || "string"; + + let mainProperty; + if (label.property) mainProperty = label.property; + else if (type === "user") mainProperty = label.properties["username"]; + else if (type === "topic") mainProperty = label.properties["title"]; + else if (type === "post") + mainProperty = label.properties["truncated_raw"]; + else mainProperty = label.properties[0]; + + return { + title: label.title, + sortProperty: label.sort_property || mainProperty, + mainProperty, + type, + compute: (row, opts = {}) => { + const value = row[mainProperty]; + + if (type === "user") return this._userLabel(label.properties, row); + if (type === "post") return this._postLabel(label.properties, row); + if (type === "topic") return this._topicLabel(label.properties, row); + if (type === "seconds") return this._secondsLabel(value); + if (type === "link") return this._linkLabel(label.properties, row); + if (type === "percent") return this._percentLabel(value); + if (type === "number") { + return this._numberLabel(value, opts); + } + if (type === "date") { + const date = moment(value, "YYYY-MM-DD"); + if (date.isValid()) return this._dateLabel(value, date); + } + if (type === "text") return this._textLabel(value); + + return { + value, + type, + property: mainProperty, + formatedValue: value ? escapeExpression(value) : "-" + }; + } + }; + }); + }, + + _userLabel(properties, row) { + const username = row[properties.username]; + + const formatedValue = () => { + const userId = row[properties.id]; + + const user = Ember.Object.create({ + username, + name: formatUsername(username), + avatar_template: row[properties.avatar] + }); + + const href = `/admin/users/${userId}/${username}`; + + const avatarImg = renderAvatar(user, { + imageSize: "tiny", + ignoreTitle: true + }); + + return `${avatarImg}${ + user.name + }`; + }; + + return { + value: username, + formatedValue: username ? formatedValue(username) : "-" + }; + }, + + _topicLabel(properties, row) { + const topicTitle = row[properties.title]; + + const formatedValue = () => { + const topicId = row[properties.id]; + const href = `/t/-/${topicId}`; + return `${topicTitle}`; + }; + + return { + value: topicTitle, + formatedValue: topicTitle ? formatedValue() : "-" + }; + }, + + _postLabel(properties, row) { + const postTitle = row[properties.truncated_raw]; + const postNumber = row[properties.number]; + const topicId = row[properties.topic_id]; + const href = `/t/-/${topicId}/${postNumber}`; + + return { + property: properties.title, + value: postTitle, + formatedValue: `${postTitle}` + }; + }, + + _secondsLabel(value) { + return { + value, + formatedValue: durationTiny(value) + }; + }, + + _percentLabel(value) { + return { + value, + formatedValue: value ? `${value}%` : "-" + }; + }, + + _numberLabel(value, options = {}) { + const formatNumbers = Ember.isEmpty(options.formatNumbers) + ? true + : options.formatNumbers; + + const formatedValue = () => (formatNumbers ? number(value) : value); + + return { + value, + formatedValue: value ? formatedValue() : "-" + }; + }, + + _dateLabel(value, date) { + return { + value, + formatedValue: value ? date.format("LL") : "-" + }; + }, + + _textLabel(value) { + const escaped = escapeExpression(value); + + return { + value, + formatedValue: value ? escaped : "-" + }; + }, + + _linkLabel(properties, row) { + const property = properties[0]; + const value = row[property]; + const formatedValue = (href, anchor) => { + return `${escapeExpression( + anchor + )}`; + }; + + return { + value, + formatedValue: value ? formatedValue(value, row[properties[1]]) : "-" + }; + }, + _computeChange(valAtT1, valAtT2) { return ((valAtT2 - valAtT1) / valAtT1) * 100; }, diff --git a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 index 5aa907b55c..b2ace398fe 100644 --- a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 @@ -5,19 +5,5 @@ export default Discourse.Route.extend({ this.controllerFor("admin-dashboard-next").fetchProblems(); this.controllerFor("admin-dashboard-next").fetchDashboard(); scrollTop(); - }, - - afterModel(model, transition) { - if (transition.targetName === "admin.dashboardNext.index") { - this.transitionTo("admin.dashboardNext.general"); - } - }, - - actions: { - willTransition(transition) { - if (transition.targetName === "admin.dashboardNext.index") { - this.transitionTo("admin.dashboardNext.general"); - } - } } }); 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 bdcdcd4a96..110ec6231b 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -3,8 +3,11 @@ export default function() { this.route("dashboard", { path: "/dashboard-old" }); this.route("dashboardNext", { path: "/" }, function() { - this.route("general", { path: "/dashboard/general" }); - this.route("moderation", { path: "/dashboard/moderation" }); + this.route("general", { path: "/" }); + this.route("admin.dashboardNextModeration", { + path: "/dashboard/moderation", + resetNamespace: true + }); }); this.route( diff --git a/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl b/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl deleted file mode 100644 index 23cc50b1f3..0000000000 Binary files a/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl and /dev/null differ diff --git a/app/assets/javascripts/admin/templates/components/admin-report-counters.hbs b/app/assets/javascripts/admin/templates/components/admin-report-counters.hbs new file mode 100644 index 0000000000..605119360c --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-report-counters.hbs @@ -0,0 +1,20 @@ +
+ {{#if model.icon}} + {{d-icon model.icon}} + {{/if}} + {{model.title}} +
+ +
{{number model.todayCount}}
+ +
+ {{number model.yesterdayCount}} {{d-icon model.yesterdayTrendIcon}} +
+ +
+ {{number model.lastSevenDaysCount}} {{d-icon model.sevenDaysTrendIcon}} +
+ +
+ {{number model.lastThirtyDaysCount}} {{d-icon model.thirtyDaysTrendIcon}} +
diff --git a/app/assets/javascripts/admin/templates/components/admin-report-table-cell.hbs b/app/assets/javascripts/admin/templates/components/admin-report-table-cell.hbs new file mode 100644 index 0000000000..973e8412be --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-report-table-cell.hbs @@ -0,0 +1 @@ +{{{formatedValue}}} diff --git a/app/assets/javascripts/admin/templates/components/admin-report-table-row.hbs b/app/assets/javascripts/admin/templates/components/admin-report-table-row.hbs index b9d0c95022..8feefaf110 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-table-row.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-table-row.hbs @@ -1,5 +1,3 @@ -{{#each cells as |cell|}} - - {{{cell.formatedValue}}} - +{{#each labels as |label|}} + {{admin-report-table-cell label=label data=data options=options}} {{/each}} diff --git a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs index 330dc6f867..1eb6923b51 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs @@ -19,35 +19,37 @@ {{#each paginatedData as |data|}} - {{admin-report-table-row data=data labels=model.computedLabels}} + {{admin-report-table-row data=data labels=model.computedLabels options=options}} {{/each}} - - -{{#if showTotalForSample}} - {{i18n 'admin.dashboard.reports.totals_for_sample'}} - - - + {{#if showTotalForSample}} + + + + {{#each totalsForSample as |total|}} - + {{/each}} - -
+ {{i18n 'admin.dashboard.reports.totals_for_sample'}} +
{{total.formatedValue}} + {{total.formatedValue}} +
-{{/if}} + {{/if}} -{{#if showTotal}} - {{i18n 'admin.dashboard.reports.total'}} - - - - - + {{#if showTotal}} + + - -
-{{number model.total}}
+ {{i18n 'admin.dashboard.reports.total'}} +
-{{/if}} + + - + {{number model.total}} + + {{/if}} + + diff --git a/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs b/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs index 133519fb98..5a5eb153e3 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs @@ -17,8 +17,7 @@
{{admin-report - startDate=startDate - endDate=endDate + filters=filters showHeader=false dataSourceName="moderators_activity"}}
@@ -27,14 +26,12 @@
{{admin-report dataSourceName="flags_status" - startDate=lastWeek reportOptions=flagsStatusOptions - endDate=endDate}} + filters=lastWeekfilters}} {{admin-report dataSourceName="post_edits" - startDate=lastWeek - endDate=endDate}} + filters=lastWeekfilters}} {{plugin-outlet name="admin-dashboard-moderation-bottom"}}
diff --git a/app/assets/javascripts/admin/templates/reports-show.hbs b/app/assets/javascripts/admin/templates/reports-show.hbs index 8649abe5db..899a8ebd9e 100644 --- a/app/assets/javascripts/admin/templates/reports-show.hbs +++ b/app/assets/javascripts/admin/templates/reports-show.hbs @@ -1,11 +1,7 @@ -
-
- {{admin-report - showAllReportsLink=true - dataSourceName=model.type - filteringState=filteringState - reportOptions=reportOptions - showFilteringUI=true - onRefresh=(action "onParamsChange")}} -
-
+{{admin-report + showAllReportsLink=true + dataSourceName=model.type + filters=filters + reportOptions=reportOptions + showFilteringUI=true + onRefresh=(action "onParamsChange")}} diff --git a/app/assets/javascripts/discourse/components/backup-codes.js.es6 b/app/assets/javascripts/discourse/components/backup-codes.js.es6 index 11457eb736..3df4b064b9 100644 --- a/app/assets/javascripts/discourse/components/backup-codes.js.es6 +++ b/app/assets/javascripts/discourse/components/backup-codes.js.es6 @@ -31,7 +31,8 @@ export default Ember.Component.extend({ } }, - @computed("formattedBackupCodes") base64BackupCode: b64EncodeUnicode, + @computed("formattedBackupCodes") + base64BackupCode: b64EncodeUnicode, @computed("backupCodes") formattedBackupCodes(backupCodes) { diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 390b5f7f28..a93267c669 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -632,7 +632,7 @@ export default Ember.Component.extend({ cacheShortUploadUrl(upload.short_url, upload.url); this.appEvents.trigger( "composer:replace-text", - uploadPlaceholder, + uploadPlaceholder.trim(), markdown ); this._resetUpload(false); diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 index 0ba2a85201..dc1eee1c3c 100644 --- a/app/assets/javascripts/discourse/components/d-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -18,7 +18,8 @@ export default Ember.Component.extend({ return hasDraft ? "topic.open_draft" : "topic.create"; }, - @computed("category.can_edit") showCategoryEdit: canEdit => canEdit, + @computed("category.can_edit") + showCategoryEdit: canEdit => canEdit, @computed("filterMode", "category", "noSubcategories") navItems(filterMode, category, noSubcategories) { diff --git a/app/assets/javascripts/discourse/components/group-post.js.es6 b/app/assets/javascripts/discourse/components/group-post.js.es6 index 5a28daf92d..f94f36dc8d 100644 --- a/app/assets/javascripts/discourse/components/group-post.js.es6 +++ b/app/assets/javascripts/discourse/components/group-post.js.es6 @@ -1,5 +1,6 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("post.url") postUrl: Discourse.getURL + @computed("post.url") + postUrl: Discourse.getURL }); diff --git a/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6 b/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6 index cee84c4bf2..2d231d1eca 100644 --- a/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6 +++ b/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6 @@ -2,7 +2,7 @@ import { default as computed, observes } from "ember-addons/ember-computed-decorators"; -import User from "discourse/models/user"; +import Group from "discourse/models/group"; import InputValidation from "discourse/models/input-validation"; import debounce from "discourse/lib/debounce"; @@ -63,7 +63,7 @@ export default Ember.Component.extend({ name = this.get("nameInput"); if (Ember.isEmpty(name)) return; - User.checkUsername(name).then(response => { + Group.checkName(name).then(response => { const validationName = "uniqueNameValidation"; if (response.available) { diff --git a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 b/app/assets/javascripts/discourse/components/topic-entrance.js.es6 index 997171aa97..216f024fc6 100644 --- a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-entrance.js.es6 @@ -32,9 +32,11 @@ export default Ember.Component.extend(CleansUp, { topic: null, visible: null, - @computed("topic.created_at") createdDate: createdAt => new Date(createdAt), + @computed("topic.created_at") + createdDate: createdAt => new Date(createdAt), - @computed("topic.bumped_at") bumpedDate: bumpedAt => new Date(bumpedAt), + @computed("topic.bumped_at") + bumpedDate: bumpedAt => new Date(bumpedAt), @computed("createdDate", "bumpedDate") showTime(createdDate, bumpedDate) { diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index 3baf8066c7..cafd20aa67 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -56,14 +56,8 @@ export default TextField.extend({ updateData: opts && opts.updateData ? opts.updateData : false, dataSource(term) { - const termRegex = Discourse.User.currentProp( - "can_send_private_email_messages" - ) - ? /[^a-zA-Z0-9_\-\.@\+]/ - : /[^a-zA-Z0-9_\-\.]/; - var results = userSearch({ - term: term.replace(termRegex, ""), + term, topicId: self.get("topicId"), exclude: excludedUsernames(), includeGroups, @@ -73,7 +67,6 @@ export default TextField.extend({ group: self.get("group"), disallowEmails }); - return results; }, diff --git a/app/assets/javascripts/discourse/components/user-stream.js.es6 b/app/assets/javascripts/discourse/components/user-stream.js.es6 index ee32cae300..f28c296849 100644 --- a/app/assets/javascripts/discourse/components/user-stream.js.es6 +++ b/app/assets/javascripts/discourse/components/user-stream.js.es6 @@ -2,6 +2,10 @@ import LoadMore from "discourse/mixins/load-more"; import ClickTrack from "discourse/lib/click-track"; import { selectedText } from "discourse/lib/utilities"; import Post from "discourse/models/post"; +import DiscourseURL from "discourse/lib/url"; +import Draft from "discourse/models/draft"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { getOwner } from "discourse-common/lib/get-owner"; export default Ember.Component.extend(LoadMore, { loading: false, @@ -57,6 +61,41 @@ export default Ember.Component.extend(LoadMore, { }); }, + resumeDraft(item) { + const composer = getOwner(this).lookup("controller:composer"); + if (composer.get("model.viewOpen")) { + composer.close(); + } + if (item.get("postUrl")) { + DiscourseURL.routeTo(item.get("postUrl")); + } else { + Draft.get(item.draft_key) + .then(d => { + if (d.draft) { + composer.open({ + draft: d.draft, + draftKey: item.draft_key, + draftSequence: d.draft_sequence + }); + } + }) + .catch(error => { + popupAjaxError(error); + }); + } + }, + + removeDraft(draft) { + const stream = this.get("stream"); + Draft.clear(draft.draft_key, draft.sequence) + .then(() => { + stream.remove(draft); + }) + .catch(error => { + popupAjaxError(error); + }); + }, + loadMore() { if (this.get("loading")) { return; diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 870f60b854..bc1764c442 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -287,7 +287,8 @@ export default Ember.Controller.extend({ return authorizesOneOrMoreExtensions(); }, - @computed() uploadIcon: () => uploadIcon(), + @computed() + uploadIcon: () => uploadIcon(), actions: { cancelUpload() { @@ -848,7 +849,10 @@ export default Ember.Controller.extend({ if (key === "new_topic") { this.send("clearTopicDraft"); } - Draft.clear(key, this.get("model.draftSequence")); + + Draft.clear(key, this.get("model.draftSequence")).then(() => { + this.appEvents.trigger("draft:destroyed", key); + }); } }, diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index 2ca1efcb83..1dd6e3d949 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -76,7 +76,7 @@ export default Ember.Controller.extend( }); return result.filter(value => { - return value.account || value.method.get("canConnect"); + return value.account || value.method.get("can_connect"); }); }, diff --git a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 index 809f80278d..e483885c91 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 @@ -1,7 +1,7 @@ import { default as computed } from "ember-addons/ember-computed-decorators"; import { default as DiscourseURL, userPath } from "discourse/lib/url"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import { LOGIN_METHODS } from "discourse/models/login-method"; +import { findAll } from "discourse/models/login-method"; export default Ember.Controller.extend({ loading: false, @@ -33,9 +33,7 @@ export default Ember.Controller.extend({ @computed displayOAuthWarning() { - return LOGIN_METHODS.some(name => { - return this.siteSettings[`enable_${name}_logins`]; - }); + return findAll().length > 0; }, toggleSecondFactor(enable) { diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6 index 6d3cbd2df1..8238d5fad9 100644 --- a/app/assets/javascripts/discourse/controllers/static.js.es6 +++ b/app/assets/javascripts/discourse/controllers/static.js.es6 @@ -7,7 +7,8 @@ export default Ember.Controller.extend({ showLoginButton: Em.computed.equal("model.path", "login"), - @computed("model.path") bodyClass: path => `static-${path}`, + @computed("model.path") + bodyClass: path => `static-${path}`, @computed("model.path") showSignupButton() { diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 index 918128d0c2..b22674657b 100644 --- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 @@ -25,9 +25,11 @@ export default Ember.Controller.extend(ModalFunctionality, { remote: Ember.computed.equal("selection", "remote"), selection: "local", - @computed() uploadIcon: () => uploadIcon(), + @computed() + uploadIcon: () => uploadIcon(), - @computed() title: () => uploadTranslate("title"), + @computed() + title: () => uploadTranslate("title"), @computed("selection") tip(selection) { diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index e952c7f1e4..96b2a333f2 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -62,6 +62,11 @@ export default Ember.Controller.extend(CanCheckEmails, { return viewingSelf || isAdmin; }, + @computed("viewingSelf") + showDrafts(viewingSelf) { + return viewingSelf; + }, + @computed("viewingSelf", "currentUser.admin") showPrivateMessages(viewingSelf, isAdmin) { return ( diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 5a5da15eb4..96ea6f2f9c 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -83,15 +83,6 @@ export default { } } - // If they right clicked, change the destination href - if (e.which === 3) { - $link.attr( - "href", - Discourse.SiteSettings.track_external_right_clicks ? destUrl : href - ); - return true; - } - // if they want to open in a new tab, do an AJAX request if (tracking && wantsNewWindow(e)) { ajax("/clicks/track", { diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index d9bced4bd4..5e3907da2c 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -31,6 +31,7 @@ const bindings = { "g b": { path: "/bookmarks" }, "g p": { path: "/my/activity" }, "g m": { path: "/my/messages" }, + "g d": { path: "/my/activity/drafts" }, home: { handler: "goToFirstPost", anonymous: true }, "command+up": { handler: "goToFirstPost", anonymous: true }, j: { handler: "selectDown", anonymous: true }, diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index cfff459ed9..6c20b968f5 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -668,6 +668,18 @@ class PluginApi { * displayName: "Discourse" * href: "https://www.discourse.org", * }) + * + * An optional `customFilter` callback can be included to not display the + * nav item on certain routes + * + * Example: + * + * addNavigationBarItem({ + * name: "link-to-bugs-category", + * displayName: "bugs" + * href: "/c/bugs", + * customFilter: (category, args) => { category && category.get('name') !== 'bug' } + * }) */ addNavigationBarItem(item) { if (!item["name"]) { diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 index aba39ef5d3..5d3fb6657d 100644 --- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 +++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 @@ -9,7 +9,7 @@ const msoListClasses = [ "MsoListParagraphCxSpLast" ]; -class Tag { +export class Tag { constructor(name, prefix = "", suffix = "", inline = false) { this.name = name; this.prefix = prefix; @@ -419,31 +419,33 @@ class Tag { } } -const tags = [ - ...Tag.blocks().map(b => Tag.block(b)), - ...Tag.headings().map((h, i) => Tag.heading(h, i + 1)), - ...Tag.slices().map(s => Tag.slice(s, "\n")), - ...Tag.emphases().map(e => Tag.emphasis(e[0], e[1])), - Tag.cell("td"), - Tag.cell("th"), - Tag.replace("br", "\n"), - Tag.replace("hr", "\n---\n"), - Tag.replace("head", ""), - Tag.keep("ins"), - Tag.keep("del"), - Tag.keep("small"), - Tag.keep("big"), - Tag.keep("kbd"), - Tag.li(), - Tag.link(), - Tag.image(), - Tag.code(), - Tag.blockquote(), - Tag.table(), - Tag.tr(), - Tag.ol(), - Tag.list("ul") -]; +function tags() { + return [ + ...Tag.blocks().map(b => Tag.block(b)), + ...Tag.headings().map((h, i) => Tag.heading(h, i + 1)), + ...Tag.slices().map(s => Tag.slice(s, "\n")), + ...Tag.emphases().map(e => Tag.emphasis(e[0], e[1])), + Tag.cell("td"), + Tag.cell("th"), + Tag.replace("br", "\n"), + Tag.replace("hr", "\n---\n"), + Tag.replace("head", ""), + Tag.keep("ins"), + Tag.keep("del"), + Tag.keep("small"), + Tag.keep("big"), + Tag.keep("kbd"), + Tag.li(), + Tag.link(), + Tag.image(), + Tag.code(), + Tag.blockquote(), + Tag.table(), + Tag.tr(), + Tag.ol(), + Tag.list("ul") + ]; +} class Element { constructor(element, parent, previous, next) { @@ -472,7 +474,8 @@ class Element { } tag() { - const tag = new (tags.filter(t => new t().name === this.name)[0] || Tag)(); + const tag = new (tags().filter(t => new t().name === this.name)[0] || + Tag)(); tag.element = this; return tag; } diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index ead4ca1fbc..0b2c13dba8 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -23,6 +23,9 @@ function performSearch( resultsFn(cached); return; } + if (term === "") { + return []; + } // need to be able to cancel this oldSearch = $.ajax(userPath("search/users"), { @@ -122,11 +125,6 @@ export default function userSearch(options) { currentTerm = term; return new Ember.RSVP.Promise(function(resolve) { - // TODO site setting for allowed regex in username - if (term.match(/[^\w_\-\.@\+]/)) { - resolve([]); - return; - } if (new Date() - cacheTime > 30000 || cacheTopicId !== topicId) { cache = {}; } diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 753c9bf741..24f4d8fb79 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -12,6 +12,7 @@ export const CREATE_TOPIC = "createTopic", EDIT_SHARED_DRAFT = "editSharedDraft", PRIVATE_MESSAGE = "privateMessage", NEW_PRIVATE_MESSAGE_KEY = "new_private_message", + NEW_TOPIC_KEY = "new_topic", REPLY = "reply", EDIT = "edit", REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic", @@ -75,7 +76,8 @@ const Composer = RestModel.extend({ return this.site.get("archetypes"); }.property(), - @computed("action") sharedDraft: action => action === CREATE_SHARED_DRAFT, + @computed("action") + sharedDraft: action => action === CREATE_SHARED_DRAFT, @computed categoryId: { @@ -133,7 +135,8 @@ const Composer = RestModel.extend({ topicFirstPost: Em.computed.or("creatingTopic", "editingFirstPost"), - @computed("action") editingPost: isEdit, + @computed("action") + editingPost: isEdit, replyingToTopic: Em.computed.equal("action", REPLY), diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index cd8034004f..1793be9524 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -8,6 +8,7 @@ import RestModel from "discourse/models/rest"; import Category from "discourse/models/category"; import User from "discourse/models/user"; import Topic from "discourse/models/topic"; +import { popupAjaxError } from "discourse/lib/ajax-error"; const Group = RestModel.extend({ limit: 50, @@ -303,6 +304,12 @@ Group.reopenClass({ messageable(name) { return ajax(`/groups/${name}/messageable`); + }, + + checkName(name) { + return ajax("/groups/check-name", { + data: { group_name: name } + }).catch(popupAjaxError); } }); diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6 index 515594b435..143deec081 100644 --- a/app/assets/javascripts/discourse/models/login-method.js.es6 +++ b/app/assets/javascripts/discourse/models/login-method.js.es6 @@ -3,38 +3,23 @@ import computed from "ember-addons/ember-computed-decorators"; const LoginMethod = Ember.Object.extend({ @computed title() { - const titleSetting = this.get("titleSetting"); - if (!Ember.isEmpty(titleSetting)) { - const result = this.siteSettings[titleSetting]; - if (!Ember.isEmpty(result)) { - return result; - } - } - return ( - this.get("titleOverride") || I18n.t(`login.${this.get("name")}.title`) + this.get("title_override") || I18n.t(`login.${this.get("name")}.title`) ); }, @computed prettyName() { - const prettyNameSetting = this.get("prettyNameSetting"); - if (!Ember.isEmpty(prettyNameSetting)) { - const result = this.siteSettings[prettyNameSetting]; - if (!Ember.isEmpty(result)) { - return result; - } - } - return ( - this.get("prettyNameOverride") || I18n.t(`login.${this.get("name")}.name`) + this.get("pretty_name_override") || + I18n.t(`login.${this.get("name")}.name`) ); }, @computed message() { return ( - this.get("messageOverride") || + this.get("message_override") || I18n.t("login." + this.get("name") + ".message") ); }, @@ -46,18 +31,9 @@ const LoginMethod = Ember.Object.extend({ if (customLogin) { customLogin(); } else { - let authUrl = this.get("customUrl") || Discourse.getURL("/auth/" + name); + let authUrl = this.get("custom_url") || Discourse.getURL("/auth/" + name); - // first check if this plugin has a site setting for full screen login before using the static setting - let fullScreenLogin = false; - const fullScreenLoginSetting = this.get("fullScreenLoginSetting"); - if (!Ember.isEmpty(fullScreenLoginSetting)) { - fullScreenLogin = this.siteSettings[fullScreenLoginSetting]; - } else { - fullScreenLogin = this.get("fullScreenLogin"); - } - - if (fullScreenLogin) { + if (this.get("full_screen_login")) { document.cookie = "fsl=true"; window.location = authUrl; } else { @@ -65,10 +41,10 @@ const LoginMethod = Ember.Object.extend({ const left = this.get("lastX") - 400; const top = this.get("lastY") - 200; - const height = this.get("frameHeight") || 400; - const width = this.get("frameWidth") || 800; + const height = this.get("frame_height") || 400; + const width = this.get("frame_width") || 800; - if (this.get("displayPopup")) { + if (name === "facebook") { authUrl = authUrl + "?display=popup"; } @@ -97,16 +73,6 @@ const LoginMethod = Ember.Object.extend({ }); let methods; -let preRegister; - -export const LOGIN_METHODS = [ - "google_oauth2", - "facebook", - "twitter", - "yahoo", - "instagram", - "github" -]; export function findAll(siteSettings, capabilities, isMobileDevice) { if (methods) { @@ -115,57 +81,16 @@ export function findAll(siteSettings, capabilities, isMobileDevice) { methods = []; - LOGIN_METHODS.forEach(name => { - if (siteSettings["enable_" + name + "_logins"]) { - const params = { name }; - if (name === "google_oauth2") { - params.frameWidth = 850; - params.frameHeight = 500; - } else if (name === "facebook") { - params.frameWidth = 580; - params.frameHeight = 400; - params.displayPopup = true; - } - - if (["facebook", "google_oauth2"].includes(name)) { - params.canConnect = true; - } - - params.siteSettings = siteSettings; - methods.pushObject(LoginMethod.create(params)); - } + Discourse.Site.currentProp("auth_providers").forEach(provider => { + methods.pushObject(LoginMethod.create(provider)); }); - if (preRegister) { - preRegister.forEach(method => { - const enabledSetting = method.get("enabledSetting"); - if (enabledSetting) { - if (siteSettings[enabledSetting]) { - methods.pushObject(method); - } - } else { - methods.pushObject(method); - } - }); - preRegister = undefined; - } - // On Mobile, Android or iOS always go with full screen if (isMobileDevice || capabilities.isIOS || capabilities.isAndroid) { - methods.forEach(m => m.set("fullScreenLogin", true)); + methods.forEach(m => m.set("full_screen_login", true)); } return methods; } -export function register(method) { - method = LoginMethod.create(method); - if (methods) { - methods.pushObject(method); - } else { - preRegister = preRegister || []; - preRegister.push(method); - } -} - export default LoginMethod; diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index 4587183ed6..9493b58704 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -1,4 +1,5 @@ import { toTitleCase } from "discourse/lib/formatter"; +import { emojiUnescape } from "discourse/lib/text"; import computed from "ember-addons/ember-computed-decorators"; const NavItem = Discourse.Model.extend({ @@ -30,7 +31,9 @@ const NavItem = Discourse.Model.extend({ extra.categoryName = toTitleCase(categoryName); } - return I18n.t(`filters.${name.replace("/", ".") + titleKey}`, extra); + return emojiUnescape( + I18n.t(`filters.${name.replace("/", ".") + titleKey}`, extra) + ); }, @computed("name") @@ -102,7 +105,9 @@ const NavItem = Discourse.Model.extend({ }); const ExtraNavItem = NavItem.extend({ - @computed("href") href: href => href + @computed("href") + href: href => href, + customFilter: null }); NavItem.reopenClass({ @@ -169,7 +174,12 @@ NavItem.reopenClass({ i => i !== null && !(category && i.get("name").indexOf("categor") === 0) ); - return items.concat(NavItem.extraNavItems); + const extraItems = NavItem.extraNavItems.filter(item => { + if (!item.customFilter) return true; + return item.customFilter.call(this, category, args); + }); + + return items.concat(extraItems); } }); diff --git a/app/assets/javascripts/discourse/models/user-draft.js.es6 b/app/assets/javascripts/discourse/models/user-draft.js.es6 new file mode 100644 index 0000000000..5004457c77 --- /dev/null +++ b/app/assets/javascripts/discourse/models/user-draft.js.es6 @@ -0,0 +1,45 @@ +import RestModel from "discourse/models/rest"; +import computed from "ember-addons/ember-computed-decorators"; +import { postUrl } from "discourse/lib/utilities"; +import { userPath } from "discourse/lib/url"; +import User from "discourse/models/user"; + +import { + NEW_TOPIC_KEY, + NEW_PRIVATE_MESSAGE_KEY +} from "discourse/models/composer"; + +export default RestModel.extend({ + @computed("draft_username") + editableDraft(draftUsername) { + return draftUsername === User.currentProp("username"); + }, + + @computed("username_lower") + userUrl(usernameLower) { + return userPath(usernameLower); + }, + + @computed("topic_id") + postUrl(topicId) { + if (!topicId) return; + + return postUrl( + this.get("slug"), + this.get("topic_id"), + this.get("post_number") + ); + }, + + @computed("draft_key") + draftType(draftKey) { + switch (draftKey) { + case NEW_TOPIC_KEY: + return I18n.t("drafts.new_topic"); + case NEW_PRIVATE_MESSAGE_KEY: + return I18n.t("drafts.new_private_message"); + default: + return false; + } + } +}); diff --git a/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6 b/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6 new file mode 100644 index 0000000000..b6d82f8a33 --- /dev/null +++ b/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6 @@ -0,0 +1,105 @@ +import { ajax } from "discourse/lib/ajax"; +import { url } from "discourse/lib/computed"; +import RestModel from "discourse/models/rest"; +import UserDraft from "discourse/models/user-draft"; +import { emojiUnescape } from "discourse/lib/text"; +import computed from "ember-addons/ember-computed-decorators"; + +import { + NEW_TOPIC_KEY, + NEW_PRIVATE_MESSAGE_KEY +} from "discourse/models/composer"; + +export default RestModel.extend({ + loaded: false, + + init() { + this._super(); + this.setProperties({ + itemsLoaded: 0, + content: [], + lastLoadedUrl: null + }); + }, + + baseUrl: url( + "itemsLoaded", + "user.username_lower", + "/drafts.json?offset=%@&username=%@" + ), + + load(site) { + this.setProperties({ + itemsLoaded: 0, + content: [], + lastLoadedUrl: null, + site: site + }); + return this.findItems(); + }, + + @computed("content.length", "loaded") + noContent(contentLength, loaded) { + return loaded && contentLength === 0; + }, + + remove(draft) { + let content = this.get("content").filter( + item => item.sequence !== draft.sequence + ); + this.setProperties({ content, itemsLoaded: content.length }); + }, + + findItems() { + let findUrl = this.get("baseUrl"); + + const lastLoadedUrl = this.get("lastLoadedUrl"); + if (lastLoadedUrl === findUrl) { + return Ember.RSVP.resolve(); + } + + if (this.get("loading")) { + return Ember.RSVP.resolve(); + } + + this.set("loading", true); + + return ajax(findUrl, { cache: "false" }) + .then(result => { + if (result && result.no_results_help) { + this.set("noContentHelp", result.no_results_help); + } + if (result && result.drafts) { + const copy = Em.A(); + result.drafts.forEach(draft => { + let draftData = JSON.parse(draft.data); + draft.post_number = draftData.postId || null; + if ( + draft.draft_key === NEW_PRIVATE_MESSAGE_KEY || + draft.draft_key === NEW_TOPIC_KEY + ) { + draft.title = draftData.title; + } + draft.title = emojiUnescape( + Handlebars.Utils.escapeExpression(draft.title) + ); + if (draft.category_id) { + draft.category = + this.site.categories.findBy("id", draft.category_id) || null; + } + + copy.pushObject(UserDraft.create(draft)); + }); + this.get("content").pushObjects(copy); + this.setProperties({ + loaded: true, + itemsLoaded: this.get("itemsLoaded") + result.drafts.length + }); + } + }) + .finally(() => { + this.set("loading", false); + this.set("lastLoadedUrl", findUrl); + }); + } +}); diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 74f103e551..b3e91306d3 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -13,11 +13,13 @@ import Badge from "discourse/models/badge"; import UserBadge from "discourse/models/user-badge"; import UserActionStat from "discourse/models/user-action-stat"; import UserAction from "discourse/models/user-action"; +import UserDraftsStream from "discourse/models/user-drafts-stream"; import Group from "discourse/models/group"; import { emojiUnescape } from "discourse/lib/text"; import PreloadStore from "preload-store"; import { defaultHomepage } from "discourse/lib/utilities"; import { userPath } from "discourse/lib/url"; +import Category from "discourse/models/category"; export const SECOND_FACTOR_METHODS = { TOTP: 1, BACKUP_CODE: 2 }; @@ -47,6 +49,11 @@ const User = RestModel.extend({ return UserPostsStream.create({ user: this }); }, + @computed() + userDraftsStream() { + return UserDraftsStream.create({ user: this }); + }, + staff: Em.computed.or("admin", "moderator"), destroySession() { @@ -198,13 +205,17 @@ const User = RestModel.extend({ return suspendedTill && moment(suspendedTill).isAfter(); }, - @computed("suspended_till") suspendedForever: isForever, + @computed("suspended_till") + suspendedForever: isForever, - @computed("silenced_till") silencedForever: isForever, + @computed("silenced_till") + silencedForever: isForever, - @computed("suspended_till") suspendedTillDate: longDate, + @computed("suspended_till") + suspendedTillDate: longDate, - @computed("silenced_till") silencedTillDate: longDate, + @computed("silenced_till") + silencedTillDate: longDate, changeUsername(new_username) { return ajax( @@ -655,6 +666,14 @@ const User = RestModel.extend({ }); } + if (summary.top_categories) { + summary.top_categories.forEach(c => { + if (c.parent_category_id) { + c.parentCategory = Category.findById(c.parent_category_id); + } + }); + } + return summary; } ); diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 63c5a2a12e..c685ba1e83 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -112,6 +112,7 @@ export default function() { this.route("likesGiven", { path: "likes-given" }); this.route("bookmarks"); this.route("pending"); + this.route("drafts"); } ); diff --git a/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6 new file mode 100644 index 0000000000..2ca94b05e5 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6 @@ -0,0 +1,22 @@ +export default Discourse.Route.extend({ + model() { + let userDraftsStream = this.modelFor("user").get("userDraftsStream"); + return userDraftsStream.load(this.site).then(() => userDraftsStream); + }, + + renderTemplate() { + this.render("user_stream"); + }, + + setupController(controller, model) { + controller.set("model", model); + this.appEvents.on("draft:destroyed", this, this.refresh); + }, + + actions: { + didTransition() { + this.controllerFor("user-activity")._showFooter(); + return true; + } + } +}); diff --git a/app/assets/javascripts/discourse/routes/user-private-messages.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages.js.es6 index a2fb6a62e8..01a6052cf4 100644 --- a/app/assets/javascripts/discourse/routes/user-private-messages.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-private-messages.js.es6 @@ -1,3 +1,5 @@ +import Draft from "discourse/models/draft"; + export default Discourse.Route.extend({ renderTemplate() { this.render("user/messages"); @@ -7,6 +9,23 @@ export default Discourse.Route.extend({ return this.modelFor("user"); }, + setupController(controller, user) { + const composerController = this.controllerFor("composer"); + controller.set("model", user); + if (this.currentUser) { + Draft.get("new_private_message").then(data => { + if (data.draft) { + composerController.open({ + draft: data.draft, + draftKey: "new_private_message", + ignoreIfChanged: true, + draftSequence: data.draft_sequence + }); + } + }); + } + }, + actions: { willTransition: function() { this._super(); diff --git a/app/assets/javascripts/discourse/routes/user.js.es6 b/app/assets/javascripts/discourse/routes/user.js.es6 index 47e4b11893..24093fd3a5 100644 --- a/app/assets/javascripts/discourse/routes/user.js.es6 +++ b/app/assets/javascripts/discourse/routes/user.js.es6 @@ -1,5 +1,3 @@ -import Draft from "discourse/models/draft"; - export default Discourse.Route.extend({ titleToken() { const username = this.modelFor("user").get("username"); @@ -67,21 +65,6 @@ export default Discourse.Route.extend({ setupController(controller, user) { controller.set("model", user); this.searchService.set("searchContext", user.get("searchContext")); - - const composerController = this.controllerFor("composer"); - controller.set("model", user); - if (this.currentUser) { - Draft.get("new_private_message").then(function(data) { - if (data.draft) { - composerController.open({ - draft: data.draft, - draftKey: "new_private_message", - ignoreIfChanged: true, - draftSequence: data.draft_sequence - }); - } - }); - } }, activate() { 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 1e6a61eef5..1f5fd34ae7 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs @@ -4,6 +4,7 @@ topic=topic openUpwards="true" toggleMultiSelect=toggleMultiSelect + hideMultiSelect=hideMultiSelect deleteTopic=deleteTopic recoverTopic=recoverTopic toggleClosed=toggleClosed 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 e4e49ecbc1..c57d183991 100644 --- a/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs @@ -1,12 +1,20 @@
{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}
{{format-date item.created_at}} - {{expand-post item=item}} + {{#if item.draftType}} + {{{item.draftType}}} + {{else}} + {{expand-post item=item}} + {{/if}}
{{topic-status topic=item disableActions=true}} - {{{item.title}}} + {{#if item.postUrl}} + {{{item.title}}} + {{else}} + {{{item.title}}} + {{/if}}
{{category-link item.category}}
@@ -50,3 +58,10 @@ {{/each}}
{{/each}} + +{{#if item.editableDraft}} +
+ {{d-button action=resumeDraft actionParam=item icon="pencil" label='drafts.resume' class="resume-draft"}} + {{d-button action=removeDraft actionParam=item icon="times" label='drafts.remove' class="remove-draft"}} +
+{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/user-stream.hbs b/app/assets/javascripts/discourse/templates/components/user-stream.hbs index e12fd05d51..1e3b3c306e 100644 --- a/app/assets/javascripts/discourse/templates/components/user-stream.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-stream.hbs @@ -1,3 +1,8 @@ {{#each stream.content as |item|}} - {{user-stream-item item=item removeBookmark=(action "removeBookmark")}} + {{user-stream-item + item=item + removeBookmark=(action "removeBookmark") + resumeDraft=(action "resumeDraft") + removeDraft=(action "removeDraft") + }} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs index a7b6c0705b..9d5ae70e07 100644 --- a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs +++ b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs @@ -14,6 +14,7 @@ {{#if siteSettings.enable_personal_messages}}
  • {{{i18n 'keyboard_shortcuts_help.jump_to.messages'}}}
  • {{/if}} +
  • {{{i18n 'keyboard_shortcuts_help.jump_to.drafts'}}}
  • {{i18n 'keyboard_shortcuts_help.navigation.title'}}