Version bump
This commit is contained in:
commit
4372f468ee
@ -48,6 +48,7 @@
|
||||
"selectKitSelectRowByValue":true,
|
||||
"selectKitSelectRowByName":true,
|
||||
"selectKitSelectRowByIndex":true,
|
||||
"keyboardHelper":true,
|
||||
"selectKitSelectNoneRow":true,
|
||||
"selectKitFillInFilter":true,
|
||||
"asyncTestDiscourse":true,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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. <a href='https://meta.discourse.org/t/prettier-code-formatting-tool/93212'>Using prettier</a>\n
|
||||
This PR doesn't match our required code formatting standards, as enforced by prettier.io. <a href='https://meta.discourse.org/t/prettier-code-formatting-tool/93212'>Here's how to set up prettier in your code editor.</a>\n
|
||||
#{prettier_offenses.map { |o| github.html_link(o) }.join("\n")}
|
||||
})
|
||||
end
|
||||
|
||||
5
Gemfile
5
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'
|
||||
|
||||
10
Gemfile.lock
10
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
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["admin-report-counters"]
|
||||
});
|
||||
@ -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")
|
||||
});
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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") || {})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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: `<a href="${escapeExpression(
|
||||
row[properties[1]]
|
||||
)}">${escapedValue}</a>`
|
||||
});
|
||||
}
|
||||
|
||||
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 `<a href='${href}'>${avatarImg}<span class='username'>${
|
||||
user.name
|
||||
}</span></a>`;
|
||||
};
|
||||
|
||||
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 `<a href='${href}'>${topicTitle}</a>`;
|
||||
};
|
||||
|
||||
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: `<a href='${href}'>${postTitle}</a>`
|
||||
};
|
||||
},
|
||||
|
||||
_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 `<a href="${escapeExpression(href)}">${escapeExpression(
|
||||
anchor
|
||||
)}</a>`;
|
||||
};
|
||||
|
||||
return {
|
||||
value,
|
||||
formatedValue: value ? formatedValue(value, row[properties[1]]) : "-"
|
||||
};
|
||||
},
|
||||
|
||||
_computeChange(valAtT1, valAtT2) {
|
||||
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
||||
},
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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(
|
||||
|
||||
Binary file not shown.
@ -0,0 +1,20 @@
|
||||
<div class="cell title">
|
||||
{{#if model.icon}}
|
||||
{{d-icon model.icon}}
|
||||
{{/if}}
|
||||
<a href="{{model.reportUrl}}">{{model.title}}</a>
|
||||
</div>
|
||||
|
||||
<div class="cell value today-count">{{number model.todayCount}}</div>
|
||||
|
||||
<div class="cell value yesterday-count {{model.yesterdayTrend}}" title={{model.yesterdayCountTitle}}>
|
||||
{{number model.yesterdayCount}} {{d-icon model.yesterdayTrendIcon}}
|
||||
</div>
|
||||
|
||||
<div class="cell value sevendays-count {{model.sevenDaysTrend}}" title={{model.sevenDaysCountTitle}}>
|
||||
{{number model.lastSevenDaysCount}} {{d-icon model.sevenDaysTrendIcon}}
|
||||
</div>
|
||||
|
||||
<div class="cell value thirty-days-count {{model.thirtyDaysTrend}}" title={{model.thirtyDaysCountTitle}}>
|
||||
{{number model.lastThirtyDaysCount}} {{d-icon model.thirtyDaysTrendIcon}}
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
{{{formatedValue}}}
|
||||
@ -1,5 +1,3 @@
|
||||
{{#each cells as |cell|}}
|
||||
<td class="{{cell.type}} {{cell.property}}" title="{{cell.tooltip}}">
|
||||
{{{cell.formatedValue}}}
|
||||
</td>
|
||||
{{#each labels as |label|}}
|
||||
{{admin-report-table-cell label=label data=data options=options}}
|
||||
{{/each}}
|
||||
|
||||
@ -19,35 +19,37 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#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}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{#if showTotalForSample}}
|
||||
<small>{{i18n 'admin.dashboard.reports.totals_for_sample'}}</small>
|
||||
<table class="totals-sample-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
{{#if showTotalForSample}}
|
||||
<tr class="total-row">
|
||||
<td colspan="{{totalsForSample.length}}">
|
||||
{{i18n 'admin.dashboard.reports.totals_for_sample'}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="admin-report-table-row">
|
||||
{{#each totalsForSample as |total|}}
|
||||
<td>{{total.formatedValue}}</td>
|
||||
<td class="{{total.type}} {{total.property}}">
|
||||
{{total.formatedValue}}
|
||||
</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showTotal}}
|
||||
<small>{{i18n 'admin.dashboard.reports.total'}}</small>
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>-</td>
|
||||
<td>{{number model.total}}</td>
|
||||
{{#if showTotal}}
|
||||
<tr class="total-row">
|
||||
<td colspan="2">
|
||||
{{i18n 'admin.dashboard.reports.total'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
<tr class="admin-report-table-row">
|
||||
<td class="date x">-</td>
|
||||
<td class="number y">{{number model.total}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination">
|
||||
{{#each pages as |pageState|}}
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
{{#if isEnabled}}
|
||||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
{{#if showTimeoutError}}
|
||||
<div class="alert alert-error">
|
||||
{{i18n "admin.dashboard.timeout_error"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showHeader}}
|
||||
<div class="report-header">
|
||||
{{#if showTitle}}
|
||||
@ -66,16 +60,29 @@
|
||||
{{/if}}
|
||||
|
||||
<div class="report-body">
|
||||
{{#unless showTimeoutError}}
|
||||
{{#if hasData}}
|
||||
{{#if currentMode}}
|
||||
{{component modeComponent model=model options=options}}
|
||||
{{#unless showError}}
|
||||
{{#if hasData}}
|
||||
{{#if currentMode}}
|
||||
{{component modeComponent model=model options=options}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="alert alert-info no-data">
|
||||
{{d-icon "pie-chart"}}
|
||||
<span>{{i18n "admin.dashboard.reports.no_data"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="alert alert-info no-data-alert">
|
||||
{{i18n 'admin.dashboard.reports.no_data'}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if showTimeoutError}}
|
||||
<div class="alert alert-error report-error timeout">
|
||||
<span>{{i18n "admin.dashboard.timeout_error"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showExceptionError}}
|
||||
<div class="alert alert-error report-error exception">
|
||||
{{i18n "admin.dashboard.exception_error"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if showFilteringUI}}
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
{{list-setting settingValue=value choices=setting.choices settingName=setting.setting}}
|
||||
{{setting-validation-message message=validationMessage}}
|
||||
<div class='desc'>{{{unbound setting.description}}}</div>
|
||||
@ -1,3 +1,3 @@
|
||||
{{list-setting settingValue=value choices=setting.choices settingName=setting.setting}}
|
||||
{{value-list values=value inputDelimiter="|" choices=setting.choices}}
|
||||
{{setting-validation-message message=validationMessage}}
|
||||
<div class='desc'>{{{unbound setting.description}}}</div>
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
{{#if collection}}
|
||||
<div class='values'>
|
||||
{{#each collection as |value index|}}
|
||||
<div class='value' draggable='true' data-index={{index}}>
|
||||
<div class='value' data-index={{index}}>
|
||||
{{d-button action="removeValue"
|
||||
actionParam=value
|
||||
icon="times"
|
||||
class="btn-small"}}
|
||||
{{value}}
|
||||
class="remove-value-btn btn-small"}}
|
||||
|
||||
{{input value=value class="value-input" focus-out=(action "changeValue" index)}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='input'>
|
||||
{{text-field value=newValue placeholderKey=addKey}}
|
||||
{{d-button action="addValue" icon="plus" class="btn-primary btn-small" disabled=inputInvalid}}
|
||||
</div>
|
||||
{{combo-box
|
||||
allowAny=true
|
||||
allowContentReplacement=true
|
||||
none=noneKey
|
||||
content=filteredChoices
|
||||
onSelect=(action "selectChoice")}}
|
||||
|
||||
@ -77,6 +77,11 @@
|
||||
{{else}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
|
||||
{{#if model.remote_theme.github_diff_link}}
|
||||
<a href="{{model.remote_theme.github_diff_link}}">
|
||||
{{i18n 'admin.customize.theme.compare_commits'}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
||||
{{/if}}
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="navigation-item moderation">
|
||||
{{#link-to "admin.dashboardNext.moderation" class="navigation-link"}}
|
||||
{{#link-to "admin.dashboardNextModeration" class="navigation-link"}}
|
||||
{{i18n "admin.dashboard.moderation_tab"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
|
||||
@ -18,43 +18,37 @@
|
||||
dataSourceName="signups"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
filters=filters}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="topics"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
filters=filters}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="posts"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
filters=filters}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="dau_by_mau"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
filters=filters}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="daily_engaged_users"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
filters=filters}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="new_contributors"
|
||||
showTrend=true
|
||||
forcedModes="chart"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
filters=filters}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -63,61 +57,50 @@
|
||||
<div class="section-columns">
|
||||
<div class="section-column">
|
||||
<div class="admin-report activity-metrics">
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.activity_metrics")}}
|
||||
<div class="report-header">
|
||||
<div class="report-title">
|
||||
<h3 class="title">
|
||||
{{#link-to "adminReports" class="report-link"}}
|
||||
{{i18n "admin.dashboard.activity_metrics"}}
|
||||
{{/link-to}}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="report-header">
|
||||
<div class="report-title">
|
||||
<h3 class="title">
|
||||
{{#link-to "adminReports" class="report-link"}}
|
||||
{{i18n "admin.dashboard.activity_metrics"}}
|
||||
{{/link-to}}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-body">
|
||||
<div class="admin-report-counters-list">
|
||||
<div class="counters-header">
|
||||
<div class="header"></div>
|
||||
<div class="header">{{i18n 'admin.dashboard.reports.today'}}</div>
|
||||
<div class="header">{{i18n 'admin.dashboard.reports.yesterday'}}</div>
|
||||
<div class="header">{{i18n 'admin.dashboard.reports.last_7_days'}}</div>
|
||||
<div class="header">{{i18n 'admin.dashboard.reports.last_30_days'}}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-body">
|
||||
<div class="admin-report-table">
|
||||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="admin-report-table-header"></th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.today'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.yesterday'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.last_7_days'}}
|
||||
</th>
|
||||
<th class="admin-report-table-header">
|
||||
{{i18n 'admin.dashboard.reports.last_30_days'}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each activityMetricsReports as |report|}}
|
||||
{{admin-report-counts report=report allTime=false class="admin-report-table-row"}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{#each activityMetrics as |metric|}}
|
||||
{{admin-report
|
||||
showHeader=false
|
||||
forcedModes="counters"
|
||||
dataSourceName=metric}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
</div>
|
||||
{{#link-to "adminReports"}}
|
||||
{{i18n "admin.dashboard.all_reports"}}
|
||||
{{/link-to}}
|
||||
|
||||
<div class="user-metrics">
|
||||
{{admin-report
|
||||
forcedModes="inline-table"
|
||||
report=usersByTypeReport
|
||||
lastRefreshedAt=lastRefreshedAt}}
|
||||
{{#conditional-loading-section isLoading=isLoading}}
|
||||
{{admin-report
|
||||
forcedModes="inline-table"
|
||||
report=usersByTypeReport
|
||||
lastRefreshedAt=lastRefreshedAt}}
|
||||
|
||||
{{admin-report
|
||||
forcedModes="inline-table"
|
||||
report=usersByTrustLevelReport
|
||||
lastRefreshedAt=lastRefreshedAt}}
|
||||
{{admin-report
|
||||
forcedModes="inline-table"
|
||||
report=usersByTrustLevelReport
|
||||
lastRefreshedAt=lastRefreshedAt}}
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}}
|
||||
@ -132,8 +115,11 @@
|
||||
</h3>
|
||||
<p>
|
||||
{{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}})
|
||||
<br />
|
||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||
|
||||
{{#if lastBackupTakenAt}}
|
||||
<br />
|
||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
@ -152,8 +138,8 @@
|
||||
<h4>{{i18n "admin.dashboard.last_updated"}} </h4>
|
||||
<p>{{updatedTimestamp}}</p>
|
||||
<a rel="noopener" target="_blank" href="https://meta.discourse.org/tags/release-notes" class="btn">
|
||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||
</a>
|
||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -167,17 +153,13 @@
|
||||
<div class="section-column">
|
||||
{{admin-report
|
||||
dataSourceName="top_referred_topics"
|
||||
reportOptions=topReferredTopicsTopions
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
reportOptions=topReferredTopicsTopions}}
|
||||
|
||||
{{admin-report
|
||||
dataSourceName="trending_search"
|
||||
reportOptions=trendingSearchOptions
|
||||
isEnabled=logSearchQueriesEnabled
|
||||
disabledLabel="admin.dashboard.reports.trending_search.disabled"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
disabledLabel="admin.dashboard.reports.trending_search.disabled"}}
|
||||
{{{i18n "admin.dashboard.reports.trending_search.more"}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,8 +17,7 @@
|
||||
|
||||
<div class="section-body">
|
||||
{{admin-report
|
||||
startDate=startDate
|
||||
endDate=endDate
|
||||
filters=filters
|
||||
showHeader=false
|
||||
dataSourceName="moderators_activity"}}
|
||||
</div>
|
||||
@ -27,14 +26,12 @@
|
||||
<div class="main-section">
|
||||
{{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"}}
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
<div class="report-container">
|
||||
<div class="visualization">
|
||||
{{admin-report
|
||||
showAllReportsLink=true
|
||||
dataSourceName=model.type
|
||||
filteringState=filteringState
|
||||
reportOptions=reportOptions
|
||||
showFilteringUI=true
|
||||
onRefresh=(action "onParamsChange")}}
|
||||
</div>
|
||||
</div>
|
||||
{{admin-report
|
||||
showAllReportsLink=true
|
||||
dataSourceName=model.type
|
||||
filters=filters
|
||||
reportOptions=reportOptions
|
||||
showFilteringUI=true
|
||||
onRefresh=(action "onParamsChange")}}
|
||||
|
||||
@ -31,7 +31,8 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@computed("formattedBackupCodes") base64BackupCode: b64EncodeUnicode,
|
||||
@computed("formattedBackupCodes")
|
||||
base64BackupCode: b64EncodeUnicode,
|
||||
|
||||
@computed("backupCodes")
|
||||
formattedBackupCodes(backupCodes) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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");
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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"]) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 = {};
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
45
app/assets/javascripts/discourse/models/user-draft.js.es6
Normal file
45
app/assets/javascripts/discourse/models/user-draft.js.es6
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
@ -112,6 +112,7 @@ export default function() {
|
||||
this.route("likesGiven", { path: "likes-given" });
|
||||
this.route("bookmarks");
|
||||
this.route("pending");
|
||||
this.route("drafts");
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
topic=topic
|
||||
openUpwards="true"
|
||||
toggleMultiSelect=toggleMultiSelect
|
||||
hideMultiSelect=hideMultiSelect
|
||||
deleteTopic=deleteTopic
|
||||
recoverTopic=recoverTopic
|
||||
toggleClosed=toggleClosed
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
<div class='clearfix info'>
|
||||
<a href={{item.userUrl}} data-user-card={{item.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
|
||||
<span class='time'>{{format-date item.created_at}}</span>
|
||||
{{expand-post item=item}}
|
||||
{{#if item.draftType}}
|
||||
<span class='draft-type'>{{{item.draftType}}}</span>
|
||||
{{else}}
|
||||
{{expand-post item=item}}
|
||||
{{/if}}
|
||||
<div class='stream-topic-details'>
|
||||
<div class='stream-topic-title'>
|
||||
{{topic-status topic=item disableActions=true}}
|
||||
<span class="title">
|
||||
<a href={{item.postUrl}}>{{{item.title}}}</a>
|
||||
{{#if item.postUrl}}
|
||||
<a href={{item.postUrl}}>{{{item.title}}}</a>
|
||||
{{else}}
|
||||
{{{item.title}}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="category">{{category-link item.category}}</div>
|
||||
@ -50,3 +58,10 @@
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if item.editableDraft}}
|
||||
<div class='user-stream-item-draft-actions'>
|
||||
{{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"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
{{#if siteSettings.enable_personal_messages}}
|
||||
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.messages'}}}</li>
|
||||
{{/if}}
|
||||
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.drafts'}}}</li>
|
||||
</ul>
|
||||
<h4>{{i18n 'keyboard_shortcuts_help.navigation.title'}}</h4>
|
||||
<ul>
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
{{#if authProvider.account}}
|
||||
<td>{{authProvider.account.description}}</td>
|
||||
<td>
|
||||
{{#if authProvider.account.can_revoke}}
|
||||
{{#if authProvider.method.can_revoke}}
|
||||
{{#conditional-loading-spinner condition=revoking size='small'}}
|
||||
{{d-button action="revokeAccount" actionParam=authProvider.account title="user.associated_accounts.revoke" icon="times-circle" }}
|
||||
{{/conditional-loading-spinner}}
|
||||
@ -119,7 +119,7 @@
|
||||
</td>
|
||||
{{else}}
|
||||
<td colspan=2>
|
||||
{{#if authProvider.method.canConnect}}
|
||||
{{#if authProvider.method.can_connect}}
|
||||
{{d-button action="connectAccount" actionParam=authProvider.method label="user.associated_accounts.connect" icon="plug" disabled=disableConnectButtons}}
|
||||
{{else}}
|
||||
{{i18n 'user.associated_accounts.not_connected'}}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<li>
|
||||
<a href title="{{user.name}}">
|
||||
{{avatar user imageSize="tiny"}}
|
||||
<span class='username'>{{user.username}}</span>
|
||||
<span class='username'>{{format-username user.username}}</span>
|
||||
<span class='name'>{{user.name}}</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -15,7 +15,7 @@
|
||||
<li>
|
||||
<a href title="{{email.username}}">
|
||||
<i class='fa fa-envelope'></i>
|
||||
<span class='username'>{{email.username}}</span>
|
||||
<span class='username'>{{format-username email.username}}</span>
|
||||
</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
||||
@ -9,6 +9,11 @@
|
||||
<li>
|
||||
{{#link-to 'userActivity.replies'}}{{i18n 'user_action_groups.5'}}{{/link-to}}
|
||||
</li>
|
||||
{{#if user.showDrafts}}
|
||||
<li>
|
||||
{{#link-to 'userActivity.drafts'}}{{i18n 'user_action_groups.15'}}{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
{{#link-to 'userActivity.likesGiven'}}{{i18n 'user_action_groups.1'}}{{/link-to}}
|
||||
</li>
|
||||
|
||||
@ -51,14 +51,15 @@ export default createWidget("hamburger-categories", {
|
||||
|
||||
html(attrs) {
|
||||
const href = Discourse.getURL("/categories");
|
||||
const result = [
|
||||
let title = I18n.t("filters.categories.title");
|
||||
if (attrs.moreCount > 0) {
|
||||
title += I18n.t("categories.more", { count: attrs.moreCount });
|
||||
}
|
||||
|
||||
let result = [
|
||||
h(
|
||||
"li.heading",
|
||||
h(
|
||||
"a.d-link.categories-link",
|
||||
{ attributes: { href } },
|
||||
I18n.t("filters.categories.title")
|
||||
)
|
||||
h("a.d-link.categories-link", { attributes: { href } }, title)
|
||||
)
|
||||
];
|
||||
|
||||
@ -66,8 +67,10 @@ export default createWidget("hamburger-categories", {
|
||||
if (categories.length === 0) {
|
||||
return;
|
||||
}
|
||||
return result.concat(
|
||||
result = result.concat(
|
||||
categories.map(c => this.attach("hamburger-category", c))
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ import { h } from "virtual-dom";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
|
||||
const flatten = array => [].concat.apply([], array);
|
||||
|
||||
@ -176,20 +177,44 @@ export default createWidget("hamburger-menu", {
|
||||
},
|
||||
|
||||
listCategories() {
|
||||
const hideUncategorized = !this.siteSettings.allow_uncategorized_topics;
|
||||
const isStaff = Discourse.User.currentProp("staff");
|
||||
const maxCategoriesToDisplay = this.siteSettings
|
||||
.header_dropdown_category_count;
|
||||
let categories = this.site.get("categoriesByCount");
|
||||
|
||||
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;
|
||||
});
|
||||
if (this.currentUser) {
|
||||
const allCategories = this.site
|
||||
.get("categories")
|
||||
.filter(c => c.notification_level !== NotificationLevels.MUTED);
|
||||
|
||||
return this.attach("hamburger-categories", { categories });
|
||||
categories = allCategories
|
||||
.filter(c => c.get("newTopics") > 0 || c.get("unreadTopics") > 0)
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
b.get("newTopics") +
|
||||
b.get("unreadTopics") -
|
||||
(a.get("newTopics") + a.get("unreadTopics"))
|
||||
);
|
||||
});
|
||||
|
||||
const topCategoryIds = this.currentUser.get("top_category_ids") || [];
|
||||
topCategoryIds.forEach(id => {
|
||||
const category = allCategories.find(c => c.id === id);
|
||||
if (category && !categories.includes(category)) {
|
||||
categories.push(category);
|
||||
}
|
||||
});
|
||||
|
||||
categories = categories.concat(
|
||||
allCategories
|
||||
.filter(c => !categories.includes(c))
|
||||
.sort((a, b) => b.topic_count - a.topic_count)
|
||||
);
|
||||
}
|
||||
|
||||
const moreCount = categories.length - maxCategoriesToDisplay;
|
||||
categories = categories.slice(0, maxCategoriesToDisplay);
|
||||
|
||||
return this.attach("hamburger-categories", { categories, moreCount });
|
||||
},
|
||||
|
||||
footerLinks(prioritizeFaq, faqUrl) {
|
||||
|
||||
@ -65,14 +65,14 @@ createWidget("pm-map-user", {
|
||||
|
||||
html(attrs) {
|
||||
const user = attrs.user;
|
||||
const avatar = avatarFor("small", {
|
||||
const avatar = avatarFor("tiny", {
|
||||
template: user.avatar_template,
|
||||
username: user.username
|
||||
});
|
||||
const link = h("a", { attributes: { href: user.get("path") } }, [
|
||||
avatar,
|
||||
" ",
|
||||
user.username
|
||||
h("span", user.username)
|
||||
]);
|
||||
const result = [link];
|
||||
const isCurrentUser = attrs.canRemoveSelfId === user.get("id");
|
||||
@ -123,7 +123,7 @@ export default createWidget("private-message-map", {
|
||||
" ",
|
||||
I18n.t("private_message_info.title")
|
||||
]),
|
||||
h("div.participants.clearfix", participants)
|
||||
h("div.participants", participants)
|
||||
];
|
||||
|
||||
if (attrs.canInvite) {
|
||||
|
||||
@ -136,6 +136,7 @@ const DEFAULT_LIST = [
|
||||
"div.quote-controls",
|
||||
"div.title",
|
||||
"div[align]",
|
||||
"div[lang]",
|
||||
"div[data-*]" /* This may seem a bit much but polls does
|
||||
it anyway and this is needed for themes,
|
||||
special code in sanitizer handles data-*
|
||||
@ -170,9 +171,11 @@ const DEFAULT_LIST = [
|
||||
"ol",
|
||||
"ol[start]",
|
||||
"p",
|
||||
"p[lang]",
|
||||
"pre",
|
||||
"s",
|
||||
"small",
|
||||
"span[lang]",
|
||||
"span.excerpt",
|
||||
"span.hashtag",
|
||||
"span.mention",
|
||||
@ -180,5 +183,12 @@ const DEFAULT_LIST = [
|
||||
"strong",
|
||||
"sub",
|
||||
"sup",
|
||||
"ul"
|
||||
"ul",
|
||||
"ruby",
|
||||
"ruby[lang]",
|
||||
"rb",
|
||||
"rb[lang]",
|
||||
"rp",
|
||||
"rt",
|
||||
"rt[lang]"
|
||||
];
|
||||
|
||||
@ -48,21 +48,15 @@ export default ComboBox.extend(Tags, {
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
$(".selected-name").off("touchend.select-kit pointerup.select-kit");
|
||||
this.$(".selected-name").off("touchend.select-kit pointerup.select-kit");
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
$(".selected-name").on(
|
||||
this.$(".selected-name").on(
|
||||
"touchend.select-kit pointerup.select-kit",
|
||||
event => {
|
||||
if (!this.get("isExpanded")) {
|
||||
this.expand(event);
|
||||
}
|
||||
|
||||
this.focusFilterOrHeader();
|
||||
}
|
||||
() => this.focusFilterOrHeader()
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -50,7 +50,6 @@ export default Ember.Component.extend(
|
||||
filterable: false,
|
||||
filter: "",
|
||||
previousFilter: "",
|
||||
filterPlaceholder: "select_kit.filter_placeholder",
|
||||
filterIcon: "search",
|
||||
headerIcon: null,
|
||||
rowComponent: "select-kit/select-kit-row",
|
||||
@ -151,9 +150,14 @@ export default Ember.Component.extend(
|
||||
this
|
||||
);
|
||||
|
||||
const existingCreatedComputedContent = this.get(
|
||||
"computedContent"
|
||||
).filterBy("created", true);
|
||||
let existingCreatedComputedContent = [];
|
||||
if (!this.get("allowContentReplacement")) {
|
||||
existingCreatedComputedContent = this.get("computedContent").filterBy(
|
||||
"created",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
computedContent: content
|
||||
.map(c => this.computeContentItem(c))
|
||||
@ -230,8 +234,8 @@ export default Ember.Component.extend(
|
||||
}
|
||||
},
|
||||
|
||||
validateCreate() {
|
||||
return !this.get("hasReachedMaximum");
|
||||
validateCreate(created) {
|
||||
return !this.get("hasReachedMaximum") && created.length > 0;
|
||||
},
|
||||
|
||||
validateSelect() {
|
||||
@ -252,10 +256,10 @@ export default Ember.Component.extend(
|
||||
return selection.length >= minimum;
|
||||
},
|
||||
|
||||
@computed("shouldFilter", "allowAny", "filter")
|
||||
shouldDisplayFilter(shouldFilter, allowAny, filter) {
|
||||
@computed("shouldFilter", "allowAny")
|
||||
shouldDisplayFilter(shouldFilter, allowAny) {
|
||||
if (shouldFilter) return true;
|
||||
if (allowAny && filter.length > 0) return true;
|
||||
if (allowAny) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
@ -285,6 +289,13 @@ export default Ember.Component.extend(
|
||||
}
|
||||
},
|
||||
|
||||
@computed("allowAny")
|
||||
filterPlaceholder(allowAny) {
|
||||
return allowAny
|
||||
? "select_kit.filter_placeholder_with_any"
|
||||
: "select_kit.filter_placeholder";
|
||||
},
|
||||
|
||||
@computed("filter", "filterable", "autoFilterable", "renderedFilterOnce")
|
||||
shouldFilter(filter, filterable, autoFilterable, renderedFilterOnce) {
|
||||
if (renderedFilterOnce && filterable) return true;
|
||||
@ -310,12 +321,7 @@ export default Ember.Component.extend(
|
||||
if (isLoading || hasReachedMaximum) return false;
|
||||
if (collectionComputedContent.map(c => c.value).includes(filter))
|
||||
return false;
|
||||
if (
|
||||
this.get("allowAny") &&
|
||||
filter.length > 0 &&
|
||||
this.validateCreate(filter)
|
||||
)
|
||||
return true;
|
||||
if (this.get("allowAny") && this.validateCreate(filter)) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export default Ember.Component.extend({
|
||||
if (computedContentTitle) return computedContentTitle;
|
||||
if (name) return name;
|
||||
|
||||
return null;
|
||||
return "";
|
||||
},
|
||||
|
||||
// this might need a more advanced solution
|
||||
|
||||
@ -77,8 +77,8 @@ export default Ember.Mixin.create({
|
||||
|
||||
// use to collapse and remove focus
|
||||
close(event) {
|
||||
this.collapse(event);
|
||||
this.setProperties({ isFocused: false });
|
||||
this.collapse(event);
|
||||
},
|
||||
|
||||
focus() {
|
||||
@ -118,8 +118,11 @@ export default Ember.Mixin.create({
|
||||
|
||||
collapse() {
|
||||
this.set("isExpanded", false);
|
||||
Ember.run.schedule("afterRender", () => this._removeFixedPosition());
|
||||
this._boundaryActionHandler("onCollapse", this);
|
||||
|
||||
Ember.run.next(() => {
|
||||
Ember.run.schedule("afterRender", () => this._removeFixedPosition());
|
||||
this._boundaryActionHandler("onCollapse", this);
|
||||
});
|
||||
},
|
||||
|
||||
// lose focus of the component in two steps
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@computed("field.value") showStaffCount: staffCount => staffCount > 1
|
||||
@computed("field.value")
|
||||
showStaffCount: staffCount => staffCount > 1
|
||||
});
|
||||
|
||||
@ -3,5 +3,6 @@ import computed from "ember-addons/ember-computed-decorators";
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [":wizard-step-form", "customStepClass"],
|
||||
|
||||
@computed("step.id") customStepClass: stepId => `wizard-step-${stepId}`
|
||||
@computed("step.id")
|
||||
customStepClass: stepId => `wizard-step-${stepId}`
|
||||
});
|
||||
|
||||
@ -31,7 +31,8 @@ export default Ember.Component.extend({
|
||||
this.autoFocus();
|
||||
},
|
||||
|
||||
@computed("step.index") showQuitButton: index => index === 0,
|
||||
@computed("step.index")
|
||||
showQuitButton: index => index === 0,
|
||||
|
||||
@computed("step.displayIndex", "wizard.totalSteps")
|
||||
showNextButton: (current, total) => current < total,
|
||||
@ -39,7 +40,8 @@ export default Ember.Component.extend({
|
||||
@computed("step.displayIndex", "wizard.totalSteps")
|
||||
showDoneButton: (current, total) => current === total,
|
||||
|
||||
@computed("step.index") showBackButton: index => index > 0,
|
||||
@computed("step.index")
|
||||
showBackButton: index => index > 0,
|
||||
|
||||
@computed("step.banner")
|
||||
bannerImage(src) {
|
||||
|
||||
@ -15,11 +15,14 @@ export default {
|
||||
this.set("_validState", States.UNCHECKED);
|
||||
},
|
||||
|
||||
@computed("_validState") valid: state => state === States.VALID,
|
||||
@computed("_validState")
|
||||
valid: state => state === States.VALID,
|
||||
|
||||
@computed("_validState") invalid: state => state === States.INVALID,
|
||||
@computed("_validState")
|
||||
invalid: state => state === States.INVALID,
|
||||
|
||||
@computed("_validState") unchecked: state => state === States.UNCHECKED,
|
||||
@computed("_validState")
|
||||
unchecked: state => state === States.UNCHECKED,
|
||||
|
||||
setValid(valid, description) {
|
||||
this.set("_validState", valid ? States.VALID : States.INVALID);
|
||||
|
||||
@ -5,7 +5,8 @@ import { ajax } from "wizard/lib/ajax";
|
||||
export default Ember.Object.extend(ValidState, {
|
||||
id: null,
|
||||
|
||||
@computed("index") displayIndex: index => index + 1,
|
||||
@computed("index")
|
||||
displayIndex: index => index + 1,
|
||||
|
||||
@computed("fields.[]")
|
||||
fieldsById(fields) {
|
||||
|
||||
@ -4,7 +4,8 @@ import { ajax } from "wizard/lib/ajax";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
const Wizard = Ember.Object.extend({
|
||||
@computed("steps.length") totalSteps: length => length,
|
||||
@computed("steps.length")
|
||||
totalSteps: length => length,
|
||||
|
||||
getTitle() {
|
||||
const titleStep = this.get("steps").findBy("id", "forum-title");
|
||||
|
||||
@ -11,69 +11,57 @@ QUnit.module("Acceptance: wizard", {
|
||||
}
|
||||
});
|
||||
|
||||
test("Wizard starts", assert => {
|
||||
visit("/");
|
||||
andThen(() => {
|
||||
assert.ok(exists(".wizard-column-contents"));
|
||||
assert.equal(currentPath(), "step");
|
||||
});
|
||||
test("Wizard starts", async assert => {
|
||||
await visit("/");
|
||||
assert.ok(exists(".wizard-column-contents"));
|
||||
assert.equal(currentPath(), "step");
|
||||
});
|
||||
|
||||
test("Going back and forth in steps", assert => {
|
||||
visit("/steps/hello-world");
|
||||
andThen(() => {
|
||||
assert.ok(exists(".wizard-step"));
|
||||
assert.ok(
|
||||
exists(".wizard-step-hello-world"),
|
||||
"it adds a class for the step id"
|
||||
);
|
||||
test("Going back and forth in steps", async assert => {
|
||||
await visit("/steps/hello-world");
|
||||
assert.ok(exists(".wizard-step"));
|
||||
assert.ok(
|
||||
exists(".wizard-step-hello-world"),
|
||||
"it adds a class for the step id"
|
||||
);
|
||||
|
||||
assert.ok(exists(".wizard-progress"));
|
||||
assert.ok(exists(".wizard-step-title"));
|
||||
assert.ok(exists(".wizard-step-description"));
|
||||
assert.ok(
|
||||
!exists(".invalid .field-full-name"),
|
||||
"don't show it as invalid until the user does something"
|
||||
);
|
||||
assert.ok(exists(".wizard-field .field-description"));
|
||||
assert.ok(!exists(".wizard-btn.back"));
|
||||
assert.ok(!exists(".wizard-field .field-error-description"));
|
||||
});
|
||||
assert.ok(exists(".wizard-progress"));
|
||||
assert.ok(exists(".wizard-step-title"));
|
||||
assert.ok(exists(".wizard-step-description"));
|
||||
assert.ok(
|
||||
!exists(".invalid .field-full-name"),
|
||||
"don't show it as invalid until the user does something"
|
||||
);
|
||||
assert.ok(exists(".wizard-field .field-description"));
|
||||
assert.ok(!exists(".wizard-btn.back"));
|
||||
assert.ok(!exists(".wizard-field .field-error-description"));
|
||||
|
||||
// invalid data
|
||||
click(".wizard-btn.next");
|
||||
andThen(() => {
|
||||
assert.ok(exists(".invalid .field-full-name"));
|
||||
});
|
||||
await click(".wizard-btn.next");
|
||||
assert.ok(exists(".invalid .field-full-name"));
|
||||
|
||||
// server validation fail
|
||||
fillIn("input.field-full-name", "Server Fail");
|
||||
click(".wizard-btn.next");
|
||||
andThen(() => {
|
||||
assert.ok(exists(".invalid .field-full-name"));
|
||||
assert.ok(exists(".wizard-field .field-error-description"));
|
||||
});
|
||||
await fillIn("input.field-full-name", "Server Fail");
|
||||
await click(".wizard-btn.next");
|
||||
assert.ok(exists(".invalid .field-full-name"));
|
||||
assert.ok(exists(".wizard-field .field-error-description"));
|
||||
|
||||
// server validation ok
|
||||
fillIn("input.field-full-name", "Evil Trout");
|
||||
click(".wizard-btn.next");
|
||||
andThen(() => {
|
||||
assert.ok(!exists(".wizard-field .field-error-description"));
|
||||
assert.ok(!exists(".wizard-step-title"));
|
||||
assert.ok(!exists(".wizard-step-description"));
|
||||
await fillIn("input.field-full-name", "Evil Trout");
|
||||
await click(".wizard-btn.next");
|
||||
assert.ok(!exists(".wizard-field .field-error-description"));
|
||||
assert.ok(!exists(".wizard-step-title"));
|
||||
assert.ok(!exists(".wizard-step-description"));
|
||||
|
||||
assert.ok(exists(".select-kit.field-snack"), "went to the next step");
|
||||
assert.ok(exists(".preview-area"), "renders the component field");
|
||||
assert.ok(exists(".select-kit.field-snack"), "went to the next step");
|
||||
assert.ok(exists(".preview-area"), "renders the component field");
|
||||
|
||||
assert.ok(!exists(".wizard-btn.next"));
|
||||
assert.ok(exists(".wizard-btn.done"), "last step shows a done button");
|
||||
assert.ok(exists(".action-link.back"), "shows the back button");
|
||||
});
|
||||
assert.ok(!exists(".wizard-btn.next"));
|
||||
assert.ok(exists(".wizard-btn.done"), "last step shows a done button");
|
||||
assert.ok(exists(".action-link.back"), "shows the back button");
|
||||
|
||||
click(".action-link.back");
|
||||
andThen(() => {
|
||||
assert.ok(exists(".wizard-step-title"));
|
||||
assert.ok(exists(".wizard-btn.next"));
|
||||
assert.ok(!exists(".wizard-prev"));
|
||||
});
|
||||
await click(".action-link.back");
|
||||
assert.ok(exists(".wizard-step-title"));
|
||||
assert.ok(exists(".wizard-btn.next"));
|
||||
assert.ok(!exists(".wizard-prev"));
|
||||
});
|
||||
|
||||
@ -8,7 +8,7 @@ componentTest("can add users", {
|
||||
this.set("field", {});
|
||||
},
|
||||
|
||||
test(assert) {
|
||||
async test(assert) {
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 0,
|
||||
"no users at first"
|
||||
@ -26,67 +26,54 @@ componentTest("can add users", {
|
||||
"it has a warning since no users were added"
|
||||
);
|
||||
|
||||
click(".add-user");
|
||||
andThen(() => {
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 0,
|
||||
"doesn't add a blank user"
|
||||
);
|
||||
assert.ok(this.$(".new-user .invalid").length === 1);
|
||||
});
|
||||
await click(".add-user");
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 0,
|
||||
"doesn't add a blank user"
|
||||
);
|
||||
assert.ok(this.$(".new-user .invalid").length === 1);
|
||||
|
||||
fillIn(".invite-email", "eviltrout@example.com");
|
||||
click(".add-user");
|
||||
await fillIn(".invite-email", "eviltrout@example.com");
|
||||
await click(".add-user");
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 1,
|
||||
"adds the user"
|
||||
);
|
||||
assert.ok(this.$(".new-user .invalid").length === 0);
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 1,
|
||||
"adds the user"
|
||||
);
|
||||
assert.ok(this.$(".new-user .invalid").length === 0);
|
||||
|
||||
const val = JSON.parse(this.get("field.value"));
|
||||
assert.equal(val.length, 1);
|
||||
assert.equal(
|
||||
val[0].email,
|
||||
"eviltrout@example.com",
|
||||
"adds the email to the JSON"
|
||||
);
|
||||
assert.ok(val[0].role.length, "adds the role to the JSON");
|
||||
assert.ok(
|
||||
!this.get("field.warning"),
|
||||
"no warning once the user is added"
|
||||
);
|
||||
});
|
||||
const val = JSON.parse(this.get("field.value"));
|
||||
assert.equal(val.length, 1);
|
||||
assert.equal(
|
||||
val[0].email,
|
||||
"eviltrout@example.com",
|
||||
"adds the email to the JSON"
|
||||
);
|
||||
assert.ok(val[0].role.length, "adds the role to the JSON");
|
||||
assert.ok(!this.get("field.warning"), "no warning once the user is added");
|
||||
|
||||
fillIn(".invite-email", "eviltrout@example.com");
|
||||
click(".add-user");
|
||||
await fillIn(".invite-email", "eviltrout@example.com");
|
||||
await click(".add-user");
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 1,
|
||||
"can't add the same user twice"
|
||||
);
|
||||
assert.ok(this.$(".new-user .invalid").length === 1);
|
||||
});
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 1,
|
||||
"can't add the same user twice"
|
||||
);
|
||||
assert.ok(this.$(".new-user .invalid").length === 1);
|
||||
|
||||
fillIn(".invite-email", "not-an-email");
|
||||
click(".add-user");
|
||||
await fillIn(".invite-email", "not-an-email");
|
||||
await click(".add-user");
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 1,
|
||||
"won't add an invalid email"
|
||||
);
|
||||
assert.ok(this.$(".new-user .invalid").length === 1);
|
||||
});
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 1,
|
||||
"won't add an invalid email"
|
||||
);
|
||||
assert.ok(this.$(".new-user .invalid").length === 1);
|
||||
|
||||
click(".invite-list .invite-list-user:eq(0) .remove-user");
|
||||
andThen(() => {
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 0,
|
||||
"removed the user"
|
||||
);
|
||||
});
|
||||
await click(".invite-list .invite-list-user:eq(0) .remove-user");
|
||||
assert.ok(
|
||||
this.$(".users-list .invite-list-user").length === 0,
|
||||
"removed the user"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -24,7 +24,7 @@ $mobile-breakpoint: 700px;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.nav.nav-pills {
|
||||
.nav-pills {
|
||||
display: inline-flex;
|
||||
width: calc(100% - 10px);
|
||||
flex: 1 0 auto;
|
||||
@ -508,7 +508,7 @@ $mobile-breakpoint: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav.nav-pills {
|
||||
.nav-pills {
|
||||
width: calc(100% - 10px);
|
||||
display: inline-flex;
|
||||
padding: 10px;
|
||||
@ -873,25 +873,43 @@ table#user-badges {
|
||||
|
||||
.value-list {
|
||||
.value {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 3px;
|
||||
margin-right: 10px;
|
||||
padding: 0.125em 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: move;
|
||||
display: flex;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.value-input {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
border-color: $primary-low;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
|
||||
&:focus {
|
||||
border-color: $tertiary;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-value-btn {
|
||||
margin-right: 0.25em;
|
||||
width: 29px;
|
||||
border: 1px solid $primary-low;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
|
||||
&:focus {
|
||||
border-color: $tertiary;
|
||||
}
|
||||
}
|
||||
}
|
||||
.values {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.placeholder {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 3px;
|
||||
margin-right: 10px;
|
||||
height: 30px;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 90%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
@ -933,6 +951,7 @@ table#user-badges {
|
||||
@import "common/admin/plugins";
|
||||
@import "common/admin/admin_reports";
|
||||
@import "common/admin/admin_report";
|
||||
@import "common/admin/admin_report_counters";
|
||||
@import "common/admin/admin_report_chart";
|
||||
@import "common/admin/admin_report_table";
|
||||
@import "common/admin/admin_report_inline_table";
|
||||
|
||||
@ -1,11 +1,35 @@
|
||||
.admin-report {
|
||||
.no-data-alert {
|
||||
.report-error,
|
||||
.no-data {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
align-self: flex-start;
|
||||
text-align: center;
|
||||
padding: 3em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.report-error {
|
||||
color: $danger;
|
||||
border: 1px solid $danger;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
background: $secondary;
|
||||
border: 1px solid $primary-low;
|
||||
color: $primary-low-mid;
|
||||
|
||||
.d-icon-pie-chart {
|
||||
color: currentColor;
|
||||
margin-bottom: 0.25em;
|
||||
font-size: $font-up-5;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.conditional-loading-section {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.report-header {
|
||||
@ -181,7 +205,7 @@
|
||||
}
|
||||
|
||||
.admin-report.post-edits {
|
||||
.admin-report-table {
|
||||
.report-table {
|
||||
table-layout: auto;
|
||||
|
||||
tbody tr td,
|
||||
@ -191,7 +215,7 @@
|
||||
|
||||
thead tr th.edit_reason,
|
||||
tbody tr td.edit_reason {
|
||||
width: 60%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
129
app/assets/stylesheets/common/admin/admin_report_counters.scss
Normal file
129
app/assets/stylesheets/common/admin/admin_report_counters.scss
Normal file
@ -0,0 +1,129 @@
|
||||
.admin-report-counters-list {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
.counters-header {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
grid-template-columns: 33% repeat(auto-fit, minmax(20px, 1fr));
|
||||
border: 1px solid $primary-low;
|
||||
border-bottom: 0;
|
||||
padding: 0.25em;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.conditional-loading-section.is-loading {
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin: 0 0 0 0.5em;
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-report.counters {
|
||||
&:last-child .admin-report-counters {
|
||||
border-bottom: 1px solid $primary-low;
|
||||
}
|
||||
|
||||
.admin-report-counters {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
grid-template-columns: 33% repeat(auto-fit, minmax(20px, 1fr));
|
||||
grid-template-rows: repeat(auto-fit, minmax(32px, 1fr));
|
||||
border: 1px solid $primary-low;
|
||||
align-items: center;
|
||||
border-bottom: 0;
|
||||
|
||||
.cell {
|
||||
padding: 0.25em;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
padding: 8px 21px 8px 8px; // accounting for negative right caret margin
|
||||
&:nth-of-type(2) {
|
||||
padding: 8px 12px 8px;
|
||||
}
|
||||
i {
|
||||
margin-right: -12px; // align on caret
|
||||
@media screen and (max-width: 650px) {
|
||||
margin-right: -9px;
|
||||
}
|
||||
}
|
||||
|
||||
&.title {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
|
||||
.d-icon {
|
||||
color: $primary-low-mid;
|
||||
min-width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
i {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
&.title {
|
||||
padding: 8px 0 8px 4px;
|
||||
font-size: $font-down-1;
|
||||
|
||||
.d-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.high-trending-up,
|
||||
&.trending-up {
|
||||
i {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
&.high-trending-down,
|
||||
&.trending-down {
|
||||
i {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: $font-0;
|
||||
border: 0;
|
||||
background: $primary-low;
|
||||
color: $primary-medium;
|
||||
.d-icon {
|
||||
font-size: $font-up-1;
|
||||
margin: 0 0.25em 0 0;
|
||||
color: $primary-low-mid;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
text-align: left;
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
@ -32,7 +32,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
.report-table {
|
||||
table-layout: fixed;
|
||||
border: 1px solid $primary-low;
|
||||
margin-top: 0;
|
||||
@ -41,16 +41,28 @@
|
||||
tbody {
|
||||
border: none;
|
||||
|
||||
.total-row {
|
||||
td {
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
td {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
|
||||
&.user {
|
||||
text-align: left;
|
||||
|
||||
.username {
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.nav.nav-pills.fields {
|
||||
.nav-pills.fields {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.content-list,
|
||||
@ -444,9 +444,17 @@
|
||||
padding: 0.25em 0;
|
||||
&.input-area {
|
||||
width: 75%;
|
||||
.value-list,
|
||||
.select-kit,
|
||||
input[type="text"] {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.value-list {
|
||||
.select-kit {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.label-area {
|
||||
width: 25%;
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
border-bottom: 10px solid $secondary;
|
||||
}
|
||||
|
||||
&.moderation .navigation-item.moderation {
|
||||
&.dashboard-next-moderation .navigation-item.moderation {
|
||||
@include active-navigation-item;
|
||||
}
|
||||
|
||||
@ -262,6 +262,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.activity-metrics {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.user-metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -330,84 +334,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.admin-report.activity-metrics {
|
||||
table {
|
||||
@media screen and (min-width: 400px) {
|
||||
table-layout: auto;
|
||||
}
|
||||
tr th {
|
||||
text-align: right;
|
||||
}
|
||||
.d-icon {
|
||||
color: $primary-low-mid;
|
||||
min-width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
@media screen and (max-width: 400px) {
|
||||
.d-icon {
|
||||
display: none;
|
||||
}
|
||||
td.title {
|
||||
padding: 8px 0 8px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
td {
|
||||
text-overflow: unset;
|
||||
overflow: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
td.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.value {
|
||||
text-align: right;
|
||||
padding: 8px 21px 8px 8px; // accounting for negative right caret margin
|
||||
&:nth-of-type(2) {
|
||||
padding: 8px 12px 8px;
|
||||
}
|
||||
i {
|
||||
margin-right: -12px; // align on caret
|
||||
@media screen and (max-width: 650px) {
|
||||
margin-right: -9px;
|
||||
}
|
||||
}
|
||||
|
||||
&.high-trending-up,
|
||||
&.trending-up {
|
||||
i {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
&.high-trending-down,
|
||||
&.trending-down {
|
||||
i {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rtl .dashboard-next {
|
||||
.section-column {
|
||||
&:last-child {
|
||||
@ -421,17 +347,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table table tbody tr {
|
||||
td.title {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td.value i {
|
||||
margin-right: 0;
|
||||
margin-left: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-metrics .table-cell {
|
||||
margin: 0 0 5px 10px;
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
}
|
||||
|
||||
#site-logo {
|
||||
max-height: 40px;
|
||||
max-height: 2.8571em;
|
||||
}
|
||||
|
||||
.d-icon-home {
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
span.edit-reason {
|
||||
background-color: lighten($highlight, 23%);
|
||||
background-color: $highlight-medium;
|
||||
padding: 3px 5px 5px 5px;
|
||||
}
|
||||
.d-icon-ban {
|
||||
|
||||
@ -55,8 +55,8 @@
|
||||
}
|
||||
|
||||
.menu-panel {
|
||||
ul.menu-links li,
|
||||
ul li.heading {
|
||||
li,
|
||||
li.heading {
|
||||
a {
|
||||
padding: 0.25em 0.5em;
|
||||
display: block;
|
||||
@ -81,6 +81,13 @@
|
||||
padding: 0.25em 0.5em;
|
||||
width: 50%;
|
||||
box-sizing: border-box;
|
||||
a {
|
||||
display: inline-flex;
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.badge-notification {
|
||||
color: dark-light-choose($primary-medium, $secondary-medium);
|
||||
background-color: transparent;
|
||||
@ -95,6 +102,7 @@
|
||||
&.bar,
|
||||
&.bullet {
|
||||
color: $primary;
|
||||
padding: 0 0 0 0.15em;
|
||||
}
|
||||
&.box {
|
||||
color: $secondary;
|
||||
|
||||
@ -254,7 +254,7 @@ aside.quote {
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
background: blend-primary-secondary(5%);
|
||||
background: $primary-very-low;
|
||||
border: 1px solid $primary-low;
|
||||
border-top: none; // would cause double top border
|
||||
|
||||
@ -292,7 +292,7 @@ aside.quote {
|
||||
|
||||
h3 {
|
||||
margin-bottom: 4px;
|
||||
color: dark-light-choose($primary-high, $secondary-low);
|
||||
color: $primary;
|
||||
line-height: $line-height-large;
|
||||
font-weight: normal;
|
||||
font-size: $font-0;
|
||||
@ -324,6 +324,65 @@ aside.quote {
|
||||
margin: 1px 5px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.participants {
|
||||
// PMs
|
||||
box-sizing: border-box;
|
||||
margin: 0 -10px;
|
||||
margin-bottom: 0.5em;
|
||||
display: flex; // IE11
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
@supports (display: grid) {
|
||||
// Overrides flex fallback above
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, auto);
|
||||
@include breakpoint(tablet, min-width) {
|
||||
grid-template-columns: repeat(5, auto);
|
||||
}
|
||||
@include breakpoint(mobile) {
|
||||
grid-template-columns: repeat(2, auto);
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
@include breakpoint(mobile-small) {
|
||||
grid-template-columns: repeat(1, auto);
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto; // IE11
|
||||
a {
|
||||
color: $primary-high;
|
||||
&[href] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
span {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
&.group {
|
||||
margin: 0;
|
||||
.d-icon {
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.d-icon-times {
|
||||
margin-left: 0.25em;
|
||||
color: dark-light-choose($primary-medium, $secondary-high);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topic-avatar,
|
||||
|
||||
@ -36,13 +36,18 @@
|
||||
}
|
||||
|
||||
.time,
|
||||
.delete-info {
|
||||
.delete-info,
|
||||
.draft-type {
|
||||
display: block;
|
||||
float: right;
|
||||
color: lighten($primary, 40%);
|
||||
font-size: $font-down-2;
|
||||
}
|
||||
|
||||
.draft-type {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
.delete-info i {
|
||||
font-size: $font-0;
|
||||
}
|
||||
@ -83,7 +88,8 @@
|
||||
padding: 3px 5px 5px 5px;
|
||||
}
|
||||
|
||||
.remove-bookmark {
|
||||
.remove-bookmark,
|
||||
.remove-draft {
|
||||
float: right;
|
||||
margin-top: -4px;
|
||||
}
|
||||
@ -95,6 +101,7 @@
|
||||
|
||||
p {
|
||||
display: inline-block;
|
||||
|
||||
span {
|
||||
color: $primary;
|
||||
}
|
||||
@ -131,7 +138,8 @@
|
||||
}
|
||||
|
||||
.user-stream .child-actions, /* DEPRECATED: '.user-stream .child-actions' selector*/
|
||||
.user-stream-item-actions {
|
||||
.user-stream-item-actions,
|
||||
.user-stream-item-draft-actions {
|
||||
margin-top: 8px;
|
||||
|
||||
.avatar-link {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user