Version bump

This commit is contained in:
Neil Lalonde 2018-08-07 12:33:08 -04:00
commit 4372f468ee
394 changed files with 7835 additions and 5322 deletions

View File

@ -48,6 +48,7 @@
"selectKitSelectRowByValue":true,
"selectKitSelectRowByName":true,
"selectKitSelectRowByIndex":true,
"keyboardHelper":true,
"selectKitSelectNoneRow":true,
"selectKitFillInFilter":true,
"asyncTestDiscourse":true,

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
classNames: ["admin-report-counters"]
});

View File

@ -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")
});

View File

@ -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;
},

View File

@ -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
});

View File

@ -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;

View File

@ -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") || {})
);
}
},

View File

@ -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 [];
}
}
});

View File

@ -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}`);
}
});

View File

@ -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}`);
}

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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({

View File

@ -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;
},

View File

@ -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");
}
}
}
});

View File

@ -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(

View File

@ -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>

View File

@ -0,0 +1 @@
{{{formatedValue}}}

View File

@ -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}}

View File

@ -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|}}

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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")}}

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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")}}

View File

@ -31,7 +31,8 @@ export default Ember.Component.extend({
}
},
@computed("formattedBackupCodes") base64BackupCode: b64EncodeUnicode,
@computed("formattedBackupCodes")
base64BackupCode: b64EncodeUnicode,
@computed("backupCodes")
formattedBackupCodes(backupCodes) {

View File

@ -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);

View File

@ -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) {

View File

@ -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
});

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
},

View File

@ -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;

View File

@ -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);
});
}
},

View File

@ -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");
});
},

View File

@ -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) {

View File

@ -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() {

View File

@ -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) {

View File

@ -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 (

View File

@ -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", {

View File

@ -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 },

View File

@ -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"]) {

View File

@ -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;
}

View File

@ -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 = {};
}

View File

@ -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),

View File

@ -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);
}
});

View File

@ -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;

View File

@ -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);
}
});

View 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;
}
}
});

View File

@ -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);
});
}
});

View File

@ -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;
}
);

View File

@ -112,6 +112,7 @@ export default function() {
this.route("likesGiven", { path: "likes-given" });
this.route("bookmarks");
this.route("pending");
this.route("drafts");
}
);

View File

@ -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;
}
}
});

View File

@ -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();

View File

@ -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() {

View File

@ -4,6 +4,7 @@
topic=topic
openUpwards="true"
toggleMultiSelect=toggleMultiSelect
hideMultiSelect=hideMultiSelect
deleteTopic=deleteTopic
recoverTopic=recoverTopic
toggleClosed=toggleClosed

View File

@ -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}}

View File

@ -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}}

View File

@ -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>

View File

@ -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'}}

View File

@ -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}}

View File

@ -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>

View File

@ -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;
}
});

View File

@ -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) {

View File

@ -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) {

View File

@ -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]"
];

View File

@ -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()
);
},

View File

@ -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;
},

View File

@ -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

View File

@ -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

View File

@ -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
});

View File

@ -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}`
});

View File

@ -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) {

View File

@ -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);

View File

@ -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) {

View File

@ -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");

View File

@ -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"));
});

View File

@ -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"
);
}
});

View File

@ -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";

View File

@ -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%;
}
}
}

View 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;
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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%;

View File

@ -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;
}

View File

@ -24,7 +24,7 @@
}
#site-logo {
max-height: 40px;
max-height: 2.8571em;
}
.d-icon-home {

View File

@ -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 {

View File

@ -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;

View File

@ -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,

View File

@ -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