REFACTOR: Support bundling our admin section as an ember addon

This commit is contained in:
Robin Ward
2020-09-22 14:18:47 -04:00
parent cfb3f4db13
commit ce3fe2f4c4
448 changed files with 130 additions and 29 deletions
@@ -0,0 +1,13 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
jsonMode: true,
basePath() {
return "/admin/api/";
},
apiNameFor() {
return "key";
},
});
@@ -0,0 +1,11 @@
import RestAdapter from "discourse/adapters/rest";
export default function buildPluginAdapter(pluginName) {
return RestAdapter.extend({
pathFor(store, type, findArgs) {
return (
"/admin/plugins/" + pluginName + this._super(store, type, findArgs)
);
},
});
}
@@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/customize/";
},
});
@@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/admin/customize/email_style";
},
});
@@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/admin/customize/embedding";
},
});
@@ -0,0 +1,2 @@
import CustomizationBase from "admin/adapters/customization-base";
export default CustomizationBase;
@@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/logs/";
},
});
@@ -0,0 +1,5 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
jsonMode: true,
});
@@ -0,0 +1,26 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/";
},
afterFindAll(results) {
let map = {};
results.forEach((theme) => {
map[theme.id] = theme;
});
results.forEach((theme) => {
let mapped = theme.get("child_themes") || [];
mapped = mapped.map((t) => map[t.id]);
theme.set("childThemes", mapped);
let mappedParents = theme.get("parent_themes") || [];
mappedParents = mappedParents.map((t) => map[t.id]);
theme.set("parentThemes", mappedParents);
});
return results;
},
jsonMode: true,
});
@@ -0,0 +1,2 @@
import CustomizationBase from "admin/adapters/customization-base";
export default CustomizationBase;
@@ -0,0 +1,7 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
},
});
@@ -0,0 +1,7 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
},
});
@@ -0,0 +1,134 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
import getURL from "discourse-common/lib/get-url";
import { observes } from "discourse-common/utils/decorators";
import { on } from "@ember/object/evented";
export default Component.extend({
mode: "css",
classNames: ["ace-wrapper"],
_editor: null,
_skipContentChangeEvent: null,
disabled: false,
@observes("editorId")
editorIdChanged() {
if (this.autofocus) {
this.send("focus");
}
},
@observes("content")
contentChanged() {
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
},
@observes("mode")
modeChanged() {
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.mode);
}
},
@observes("placeholder")
placeholderChanged() {
if (this._editor) {
this._editor.setOptions({
placeholder: this.placeholder,
});
}
},
@observes("disabled")
disabledStateChanged() {
this.changeDisabledState();
},
changeDisabledState() {
const editor = this._editor;
if (editor) {
const disabled = this.disabled;
editor.setOptions({
readOnly: disabled,
highlightActiveLine: !disabled,
highlightGutterLine: !disabled,
});
editor.container.parentNode.setAttribute("data-disabled", disabled);
}
},
_destroyEditor: on("willDestroyElement", function () {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.off("ace:resize", this, "resize");
}
$(window).off("ace:resize");
}),
resize() {
if (this._editor) {
this._editor.resize();
}
},
didInsertElement() {
this._super(...arguments);
loadScript("/javascripts/ace/ace.js").then(() => {
window.ace.require(["ace/ace"], (loadedAce) => {
loadedAce.config.set("loadWorkerFromBlob", false);
loadedAce.config.set("workerPath", getURL("/javascripts/ace")); // Do not use CDN for workers
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
const editor = loadedAce.edit(this.element.querySelector(".ace"));
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.setOptions({ fontSize: "14px", placeholder: this.placeholder });
editor.getSession().setMode("ace/mode/" + this.mode);
editor.on("change", () => {
this._skipContentChangeEvent = true;
this.set("content", editor.getSession().getValue());
this._skipContentChangeEvent = false;
});
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10, 10);
this.element.setAttribute("data-editor", editor);
this._editor = editor;
this.changeDisabledState();
$(window)
.off("ace:resize")
.on("ace:resize", () => this.appEvents.trigger("ace:resize"));
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.on("ace:resize", this, "resize");
}
if (this.autofocus) {
this.send("focus");
}
});
});
},
actions: {
focus() {
if (this._editor) {
this._editor.focus();
this._editor.navigateFileEnd();
}
},
},
});
@@ -0,0 +1,76 @@
import I18n from "I18n";
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import discourseDebounce from "discourse/lib/debounce";
import { observes, on } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["admin-backups-logs"],
showLoadingSpinner: false,
hasFormattedLogs: false,
noLogsMessage: I18n.t("admin.backups.logs.none"),
init() {
this._super(...arguments);
this._reset();
},
_reset() {
this.setProperties({ formattedLogs: "", index: 0 });
},
_scrollDown() {
const div = this.element;
div.scrollTop = div.scrollHeight;
},
@on("init")
@observes("logs.[]")
_resetFormattedLogs() {
if (this.logs.length === 0) {
this._reset(); // reset the cached logs whenever the model is reset
this.renderLogs();
}
},
@on("init")
@observes("logs.[]")
_updateFormattedLogs: discourseDebounce(function () {
const logs = this.logs;
if (logs.length === 0) {
return;
}
// do the log formatting only once for HELLish performance
let formattedLogs = this.formattedLogs;
for (let i = this.index, length = logs.length; i < length; i++) {
const date = logs[i].get("timestamp"),
message = logs[i].get("message");
formattedLogs += "[" + date + "] " + message + "\n";
}
// update the formatted logs & cache index
this.setProperties({
formattedLogs: formattedLogs,
index: logs.length,
});
// force rerender
this.renderLogs();
scheduleOnce("afterRender", this, this._scrollDown);
}, 150),
renderLogs() {
const formattedLogs = this.formattedLogs;
if (formattedLogs && formattedLogs.length > 0) {
this.set("hasFormattedLogs", true);
} else {
this.set("hasFormattedLogs", false);
}
// add a loading indicator
if (this.get("status.isOperationRunning")) {
this.set("showLoadingSpinner", true);
} else {
this.set("showLoadingSpinner", false);
}
},
});
@@ -0,0 +1,24 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
buffer: "",
editing: false,
init() {
this._super(...arguments);
this.set("editing", false);
},
actions: {
edit() {
this.set("buffer", this.value);
this.toggleProperty("editing");
},
save() {
// Action has to toggle 'editing' property.
this.action(this.buffer);
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["row"],
});
@@ -0,0 +1,57 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
tagName: "canvas",
type: "line",
refreshChart() {
const ctx = this.element.getContext("2d");
const model = this.model;
const rawData = this.get("model.data");
var data = {
labels: rawData.map((r) => r.x),
datasets: [
{
data: rawData.map((r) => r.y),
label: model.get("title"),
backgroundColor: `rgba(200,220,240,${this.type === "bar" ? 1 : 0.3})`,
borderColor: "#08C",
},
],
};
const config = {
type: this.type,
data: data,
options: {
responsive: true,
tooltips: {
callbacks: {
title: (context) =>
moment(context[0].xLabel, "YYYY-MM-DD").format("LL"),
},
},
scales: {
yAxes: [
{
display: true,
ticks: {
stepSize: 1,
},
},
],
},
},
};
this._chart = new window.Chart(ctx, config);
},
didInsertElement() {
loadScript("/javascripts/Chart.min.js").then(() =>
this.refreshChart.apply(this)
);
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});
@@ -0,0 +1,239 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart"],
limit: 8,
total: 0,
options: null,
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
this._renderChart(
this.model,
this.element && this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) {
return;
}
const context = chartCanvas.getContext("2d");
const chartData = this._applyChartGrouping(
model,
makeArray(model.get("chartData") || model.get("data"), "weekly"),
this.options
);
const prevChartData = makeArray(
model.get("prevChartData") || model.get("prev_data")
);
const labels = chartData.map((d) => d.x);
const data = {
labels,
datasets: [
{
data: chartData.map((d) => Math.round(parseFloat(d.y))),
backgroundColor: prevChartData.length
? "transparent"
: model.secondary_color,
borderColor: model.primary_color,
pointRadius: 3,
borderWidth: 1,
pointBackgroundColor: model.primary_color,
pointBorderColor: model.primary_color,
},
],
};
if (prevChartData.length) {
data.datasets.push({
data: prevChartData.map((d) => Math.round(parseFloat(d.y))),
borderColor: model.primary_color,
borderDash: [5, 5],
backgroundColor: "transparent",
borderWidth: 1,
pointRadius: 0,
});
}
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
if (!this.element) {
return;
}
this._chart = new window.Chart(
context,
this._buildChartConfig(data, this.options)
);
});
},
_buildChartConfig(data, options) {
return {
type: "line",
data,
options: {
tooltips: {
callbacks: {
title: (tooltipItem) =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL"),
},
},
legend: {
display: false,
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
animation: {
duration: 0,
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
},
scales: {
yAxes: [
{
display: true,
ticks: {
userCallback: (label) => {
if (Math.floor(label) === label) {
return label;
}
},
callback: (label) => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25,
},
},
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
unit: this._unitForGrouping(options),
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
},
_applyChartGrouping(model, data, options) {
if (!options.chartGrouping || options.chartGrouping === "daily") {
return data;
}
if (
options.chartGrouping === "weekly" ||
options.chartGrouping === "monthly"
) {
const isoKind = options.chartGrouping === "weekly" ? "isoWeek" : "month";
const kind = options.chartGrouping === "weekly" ? "week" : "month";
const startMoment = moment(model.start_date, "YYYY-MM-DD");
let currentIndex = 0;
let currentStart = startMoment.clone().startOf(isoKind);
let currentEnd = startMoment.clone().endOf(isoKind);
const transformedData = [
{
x: currentStart.format("YYYY-MM-DD"),
y: 0,
},
];
data.forEach((d) => {
let date = moment(d.x, "YYYY-MM-DD");
if (!date.isBetween(currentStart, currentEnd)) {
currentIndex += 1;
currentStart = currentStart.add(1, kind).startOf(isoKind);
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
}
if (transformedData[currentIndex]) {
transformedData[currentIndex].y += d.y;
} else {
transformedData[currentIndex] = {
x: d.x,
y: d.y,
};
}
});
return transformedData;
}
// ensure we return something if grouping is unknown
return data;
},
_unitForGrouping(options) {
switch (options.chartGrouping) {
case "monthly":
return "month";
case "weekly":
return "week";
default:
return "day";
}
},
});
@@ -0,0 +1,6 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-counters"],
attributeBindings: ["model.description:title"],
});
@@ -0,0 +1,11 @@
import { match } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
allTime: true,
tagName: "tr",
reverseColors: match(
"report.type",
/^(time_to_first_response|topics_with_no_response)$/
),
classNameBindings: ["reverseColors"],
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-inline-table"],
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});
@@ -0,0 +1,159 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart", "admin-report-stacked-chart"],
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) {
return;
}
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map((cd) => {
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data.map((d) => Math.round(parseFloat(d.y))),
backgroundColor: cd.color,
};
}),
};
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "bar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0,
},
tooltips: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: (tooltipItem) => {
let total = 0;
tooltipItem.forEach(
(item) => (total += parseInt(item.yLabel || 0, 10))
);
return `= ${total}`;
},
title: (tooltipItem) =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL"),
},
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
},
scales: {
yAxes: [
{
stacked: true,
display: true,
ticks: {
userCallback: (label) => {
if (Math.floor(label) === label) {
return label;
}
},
callback: (label) => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25,
},
},
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
offset: true,
time: {
parser: "YYYY-MM-DD",
minUnit: "day",
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
},
});
@@ -0,0 +1,43 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import { setting } from "discourse/lib/computed";
export default Component.extend({
classNames: ["admin-report-storage-stats"],
backupLocation: setting("backup_location"),
backupStats: alias("model.data.backups"),
uploadStats: alias("model.data.uploads"),
@discourseComputed("backupStats")
showBackupStats(stats) {
return stats && this.currentUser.admin;
},
@discourseComputed("backupLocation")
backupLocationName(backupLocation) {
return I18n.t(`admin.backups.location.${backupLocation}`);
},
@discourseComputed("backupStats.used_bytes")
usedBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("backupStats.free_bytes")
freeBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("uploadStats.used_bytes")
usedUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("uploadStats.free_bytes")
freeUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
},
});
@@ -0,0 +1,20 @@
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
tagName: "td",
classNames: ["admin-report-table-cell"],
classNameBindings: ["type", "property"],
options: null,
@discourseComputed("label", "data", "options")
computedLabel(label, data, options) {
return label.compute(data, options || {});
},
type: alias("label.type"),
property: alias("label.mainProperty"),
formatedValue: alias("computedLabel.formatedValue"),
value: alias("computedLabel.value"),
});
@@ -0,0 +1,19 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
export default Component.extend({
tagName: "th",
classNames: ["admin-report-table-header"],
classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"],
attributeBindings: ["label.title:title"],
@discourseComputed("currentSortLabel.sortProperty", "label.sortProperty")
isCurrentSort(currentSortField, labelSortField) {
return currentSortField === labelSortField;
},
@discourseComputed("currentSortDirection")
sortIcon(currentSortDirection) {
return currentSortDirection === 1 ? "caret-up" : "caret-down";
},
});
@@ -0,0 +1,6 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
classNames: ["admin-report-table-row"],
options: null,
});
@@ -0,0 +1,174 @@
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
const PAGES_LIMIT = 8;
export default Component.extend({
classNameBindings: ["sortable", "twoColumns"],
classNames: ["admin-report-table"],
sortable: false,
sortDirection: 1,
perPage: alias("options.perPage"),
page: 0,
@discourseComputed("model.computedLabels.length")
twoColumns(labelsLength) {
return labelsLength === 2;
},
@discourseComputed(
"totalsForSample",
"options.total",
"model.dates_filtering"
)
showTotalForSample(totalsForSample, total, datesFiltering) {
// check if we have at least one cell which contains a value
const sum = totalsForSample
.map((t) => t.value)
.compact()
.reduce((s, v) => s + v, 0);
return sum >= 1 && total && datesFiltering;
},
@discourseComputed("model.total", "options.total", "twoColumns")
showTotal(reportTotal, total, twoColumns) {
return reportTotal && total && twoColumns;
},
@discourseComputed(
"model.{average,data}",
"totalsForSample.1.value",
"twoColumns"
)
showAverage(model, sampleTotalValue, hasTwoColumns) {
return (
model.average &&
model.data.length > 0 &&
sampleTotalValue &&
hasTwoColumns
);
},
@discourseComputed("totalsForSample.1.value", "model.data.length")
averageForSample(totals, count) {
return (totals / count).toFixed(0);
},
@discourseComputed("model.data.length")
showSortingUI(dataLength) {
return dataLength >= 5;
},
@discourseComputed("totalsForSampleRow", "model.computedLabels")
totalsForSample(row, labels) {
return labels.map((label) => {
const computedLabel = label.compute(row);
computedLabel.type = label.type;
computedLabel.property = label.mainProperty;
return computedLabel;
});
},
@discourseComputed("model.data", "model.computedLabels")
totalsForSampleRow(rows, labels) {
if (!rows || !rows.length) {
return {};
}
let totalsRow = {};
labels.forEach((label) => {
const reducer = (sum, row) => {
const computedLabel = label.compute(row);
const value = computedLabel.value;
if (!["seconds", "number", "percent"].includes(label.type)) {
return;
} else {
return sum + Math.round(value || 0);
}
};
const total = rows.reduce(reducer, 0);
totalsRow[label.mainProperty] =
label.type === "percent" ? Math.round(total / rows.length) : total;
});
return totalsRow;
},
@discourseComputed("sortLabel", "sortDirection", "model.data.[]")
sortedData(sortLabel, sortDirection, data) {
data = makeArray(data);
if (sortLabel) {
const compare = (label, direction) => {
return (a, b) => {
const aValue = label.compute(a, { useSortProperty: true }).value;
const bValue = label.compute(b, { useSortProperty: true }).value;
const result = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
return result * direction;
};
};
return data.sort(compare(sortLabel, sortDirection));
}
return data;
},
@discourseComputed("sortedData.[]", "perPage", "page")
paginatedData(data, perPage, page) {
if (perPage < data.length) {
const start = perPage * page;
return data.slice(start, start + perPage);
}
return data;
},
@discourseComputed("model.data", "perPage", "page")
pages(data, perPage, page) {
if (!data || data.length <= perPage) {
return [];
}
const pagesIndexes = [];
for (let i = 0; i < Math.ceil(data.length / perPage); i++) {
pagesIndexes.push(i);
}
let pages = pagesIndexes.map((v) => {
return {
page: v + 1,
index: v,
class: v === page ? "is-current" : null,
};
});
if (pages.length > PAGES_LIMIT) {
const before = Math.max(0, page - PAGES_LIMIT / 2);
const after = Math.max(PAGES_LIMIT, page + PAGES_LIMIT / 2);
pages = pages.slice(before, after);
}
return pages;
},
actions: {
changePage(page) {
this.set("page", page);
},
sortByLabel(label) {
if (this.sortLabel === label) {
this.set("sortDirection", this.sortDirection === 1 ? -1 : 1);
} else {
this.set("sortLabel", label);
}
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});
@@ -0,0 +1,453 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { alias, or, and, equal, notEmpty } from "@ember/object/computed";
import EmberObject, { computed, action } from "@ember/object";
import { next } from "@ember/runloop";
import Component from "@ember/component";
import ReportLoader from "discourse/lib/reports-loader";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import Report, { SCHEMA_VERSION } from "admin/models/report";
import { isPresent } from "@ember/utils";
import { isTesting } from "discourse-common/config/environment";
const TABLE_OPTIONS = {
perPage: 8,
total: true,
limit: 20,
formatNumbers: true,
};
const CHART_OPTIONS = {};
function collapseWeekly(data, average) {
let aggregate = [];
let bucket, i;
let offset = data.length % 7;
for (i = offset; i < data.length; i++) {
if (bucket && i % 7 === offset) {
if (average) {
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
}
aggregate.push(bucket);
bucket = null;
}
bucket = bucket || { x: data[i].x, y: 0 };
bucket.y += data[i].y;
}
return aggregate;
}
export default Component.extend({
classNameBindings: [
"isHidden:hidden",
"isHidden::is-visible",
"isEnabled",
"isLoading",
"dasherizedDataSourceName",
],
classNames: ["admin-report"],
isEnabled: true,
disabledLabel: I18n.t("admin.dashboard.disabled"),
isLoading: false,
rateLimitationString: null,
dataSourceName: null,
report: null,
model: null,
reportOptions: null,
forcedModes: null,
showAllReportsLink: false,
filters: null,
showTrend: false,
showHeader: true,
showTitle: true,
showFilteringUI: false,
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
init() {
this._super(...arguments);
this._reports = [];
},
isHidden: computed("siteSettings.dashboard_hidden_reports", function () {
return (this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean)
.includes(this.dataSourceName);
}),
startDate: computed("filters.startDate", function () {
if (this.filters && isPresent(this.filters.startDate)) {
return moment(this.filters.startDate, "YYYY-MM-DD");
} else {
return moment();
}
}),
endDate: computed("filters.endDate", function () {
if (this.filters && isPresent(this.filters.endDate)) {
return moment(this.filters.endDate, "YYYY-MM-DD");
} else {
return moment();
}
}),
didReceiveAttrs() {
this._super(...arguments);
if (this.report) {
this._renderReport(this.report, this.forcedModes, this.currentMode);
} else if (this.dataSourceName) {
this._fetchReport();
}
},
showError: or("showTimeoutError", "showExceptionError", "showNotFoundError"),
showNotFoundError: equal("model.error", "not_found"),
showTimeoutError: equal("model.error", "timeout"),
showExceptionError: equal("model.error", "exception"),
hasData: notEmpty("model.data"),
@discourseComputed("dataSourceName", "model.type")
dasherizedDataSourceName(dataSourceName, type) {
return (dataSourceName || type || "undefined").replace(/_/g, "-");
},
@discourseComputed("dataSourceName", "model.type")
dataSource(dataSourceName, type) {
dataSourceName = dataSourceName || type;
return `/admin/reports/${dataSourceName}`;
},
@discourseComputed("displayedModes.length")
showModes(displayedModesLength) {
return displayedModesLength > 1;
},
@discourseComputed("currentMode")
isChartMode(currentMode) {
return currentMode === "chart";
},
@action
changeGrouping(grouping) {
this.send("refreshReport", {
chartGrouping: grouping,
});
},
@discourseComputed("currentMode", "model.modes", "forcedModes")
displayedModes(currentMode, reportModes, forcedModes) {
const modes = forcedModes ? forcedModes.split(",") : reportModes;
return makeArray(modes).map((mode) => {
const base = `btn-default mode-btn ${mode}`;
const cssClass = currentMode === mode ? `${base} is-current` : base;
return {
mode,
cssClass,
icon: mode === "table" ? "table" : "signal",
};
});
},
@discourseComputed("currentMode")
modeComponent(currentMode) {
return `admin-report-${currentMode.replace(/_/g, "-")}`;
},
@discourseComputed(
"dataSourceName",
"startDate",
"endDate",
"filters.customFilters"
)
reportKey(dataSourceName, startDate, endDate, customFilters) {
if (!dataSourceName || !startDate || !endDate) {
return null;
}
startDate = startDate.toISOString(true).split("T")[0];
endDate = endDate.toISOString(true).split("T")[0];
let reportKey = "reports:";
reportKey += [
dataSourceName,
isTesting() ? "start" : startDate.replace(/-/g, ""),
isTesting() ? "end" : endDate.replace(/-/g, ""),
"[:prev_period]",
this.get("reportOptions.table.limit"),
// Convert all filter values to strings to ensure unique serialization
customFilters
? JSON.stringify(customFilters, (k, v) => (k ? `${v}` : v))
: null,
SCHEMA_VERSION,
]
.filter((x) => x)
.map((x) => x.toString())
.join(":");
return reportKey;
},
@discourseComputed("reportOptions.chartGrouping")
chartGroupings(chartGrouping) {
chartGrouping = chartGrouping || "daily";
return ["daily", "weekly", "monthly"].map((id) => {
return {
id,
label: `admin.dashboard.reports.${id}`,
class: `chart-grouping ${chartGrouping === id ? "active" : "inactive"}`,
};
});
},
@action
onChangeDateRange(range) {
this.send("refreshReport", {
startDate: range.from,
endDate: range.to,
});
},
@action
applyFilter(id, value) {
let customFilters = this.get("filters.customFilters") || {};
if (typeof value === "undefined") {
delete customFilters[id];
} else {
customFilters[id] = value;
}
this.send("refreshReport", {
filters: customFilters,
});
},
@action
refreshReport(options = {}) {
if (!this.attrs.onRefresh) {
return;
}
this.attrs.onRefresh({
type: this.get("model.type"),
chartGrouping: options.chartGrouping,
startDate:
typeof options.startDate === "undefined"
? this.startDate
: options.startDate,
endDate:
typeof options.endDate === "undefined" ? this.endDate : options.endDate,
filters:
typeof options.filters === "undefined"
? this.get("filters.customFilters")
: options.filters,
});
},
@action
exportCsv() {
const args = {
name: this.get("model.type"),
start_date: this.startDate.toISOString(true).split("T")[0],
end_date: this.endDate.toISOString(true).split("T")[0],
};
const customFilters = this.get("filters.customFilters");
if (customFilters) {
Object.assign(args, customFilters);
}
exportEntity("report", args).then(outputExportResult);
},
@action
changeMode(mode) {
this.set("currentMode", mode);
this.send("refreshReport", {
chartGrouping: null,
});
},
_computeReport() {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
if (!this._reports || !this._reports.length) {
return;
}
// on a slow network _fetchReport could be called multiple times between
// T and T+x, and all the ajax responses would occur after T+(x+y)
// to avoid any inconsistencies we filter by period and make sure
// the array contains only unique values
let filteredReports = this._reports.uniqBy("report_key");
let report;
const sort = (r) => {
if (r.length > 1) {
return r.findBy("type", this.dataSourceName);
} else {
return r;
}
};
if (!this.startDate || !this.endDate) {
report = sort(filteredReports)[0];
} else {
report = sort(
filteredReports.filter((r) => r.report_key.includes(this.reportKey))
)[0];
if (!report) {
return;
}
}
if (report.error === "not_found") {
this.set("showFilteringUI", false);
}
this._renderReport(report, this.forcedModes, this.currentMode);
},
_renderReport(report, forcedModes, currentMode) {
const modes = forcedModes ? forcedModes.split(",") : report.modes;
currentMode = currentMode || (modes ? modes[0] : null);
this.setProperties({
model: report,
currentMode,
options: this._buildOptions(currentMode),
});
},
_fetchReport() {
this._super(...arguments);
this.setProperties({ isLoading: true, rateLimitationString: null });
next(() => {
let payload = this._buildPayload(["prev_period"]);
const callback = (response) => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
this.set("isLoading", false);
if (response === 429) {
this.set(
"rateLimitationString",
I18n.t("admin.dashboard.too_many_requests")
);
} else if (response === 500) {
this.set("model.error", "exception");
} else if (response) {
this._reports.push(this._loadReport(response));
this._computeReport();
}
};
ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
});
},
_buildPayload(facets) {
let payload = { data: { cache: true, facets } };
if (this.startDate) {
payload.data.start_date = moment(this.startDate)
.toISOString(true)
.split("T")[0];
}
if (this.endDate) {
payload.data.end_date = moment(this.endDate)
.toISOString(true)
.split("T")[0];
}
if (this.get("reportOptions.table.limit")) {
payload.data.limit = this.get("reportOptions.table.limit");
}
if (this.get("filters.customFilters")) {
payload.data.filters = this.get("filters.customFilters");
}
return payload;
},
_buildOptions(mode) {
if (mode === "table") {
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
return EmberObject.create(
Object.assign(tableOptions, this.get("reportOptions.table") || {})
);
} else {
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
return EmberObject.create(
Object.assign(chartOptions, this.get("reportOptions.chart") || {}, {
chartGrouping: this.get("reportOptions.chartGrouping"),
})
);
}
},
_loadReport(jsonReport) {
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
jsonReport.chartData = jsonReport.chartData.map((chartData) => {
if (chartData.length > 40) {
return {
data: collapseWeekly(chartData.data),
req: chartData.req,
label: chartData.label,
color: chartData.color,
};
} else {
return chartData;
}
});
} else if (jsonReport.chartData && jsonReport.chartData.length > 40) {
jsonReport.chartData = collapseWeekly(
jsonReport.chartData,
jsonReport.average
);
}
if (jsonReport.prev_data) {
Report.fillMissingDates(jsonReport, {
filledField: "prevChartData",
dataField: "prev_data",
starDate: jsonReport.prev_startDate,
endDate: jsonReport.prev_endDate,
});
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
jsonReport.prevChartData = collapseWeekly(
jsonReport.prevChartData,
jsonReport.average
);
}
}
return Report.create(jsonReport);
},
});
@@ -0,0 +1,115 @@
import I18n from "I18n";
import { next } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
export default Component.extend({
@discourseComputed("theme.targets", "onlyOverridden", "showAdvanced")
visibleTargets(targets, onlyOverridden, showAdvanced) {
return targets.filter((target) => {
if (target.advanced && !showAdvanced) {
return false;
}
if (!onlyOverridden) {
return true;
}
return target.edited;
});
},
@discourseComputed("currentTargetName", "onlyOverridden", "theme.fields")
visibleFields(targetName, onlyOverridden, fields) {
fields = fields[targetName];
if (onlyOverridden) {
fields = fields.filter((field) => field.edited);
}
return fields;
},
@discourseComputed("currentTargetName", "fieldName")
activeSectionMode(targetName, fieldName) {
if (["settings", "translations"].includes(targetName)) {
return "yaml";
}
if (["extra_scss"].includes(targetName)) {
return "scss";
}
if (["color_definitions"].includes(fieldName)) {
return "scss";
}
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
},
@discourseComputed("currentTargetName", "fieldName")
placeholder(targetName, fieldName) {
return fieldName && fieldName === "color_definitions"
? I18n.t("admin.customize.theme.color_definitions.placeholder")
: "";
},
@discourseComputed("fieldName", "currentTargetName", "theme")
activeSection: {
get(fieldName, target, model) {
return model.getField(target, fieldName);
},
set(value, fieldName, target, model) {
model.setField(target, fieldName, value);
return value;
},
},
editorId: fmt("fieldName", "currentTargetName", "%@|%@"),
@discourseComputed("maximized")
maximizeIcon(maximized) {
return maximized ? "discourse-compress" : "discourse-expand";
},
@discourseComputed("currentTargetName", "theme.targets")
showAddField(currentTargetName, targets) {
return targets.find((t) => t.name === currentTargetName).customNames;
},
@discourseComputed(
"currentTargetName",
"fieldName",
"theme.theme_fields.@each.error"
)
error(target, fieldName) {
return this.theme.getError(target, fieldName);
},
actions: {
toggleShowAdvanced() {
this.toggleProperty("showAdvanced");
},
toggleAddField() {
this.toggleProperty("addingField");
},
cancelAddField() {
this.set("addingField", false);
},
addField(name) {
if (!name) {
return;
}
name = name.replace(/[^a-zA-Z0-9-_/]/g, "");
this.theme.setField(this.currentTargetName, name, "");
this.setProperties({ newFieldName: "", addingField: false });
this.fieldAdded(this.currentTargetName, name);
},
toggleMaximize: function () {
this.toggleProperty("maximized");
next(() => this.appEvents.trigger("ace:resize"));
},
onlyOverriddenChanged(value) {
this.onlyOverriddenChanged(value);
},
},
});
@@ -0,0 +1,108 @@
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { empty } from "@ember/object/computed";
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import UserField from "admin/models/user-field";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyEqual } from "discourse/lib/computed";
import { i18n } from "discourse/lib/computed";
import discourseComputed, {
observes,
on,
} from "discourse-common/utils/decorators";
export default Component.extend(bufferedProperty("userField"), {
editing: empty("userField.id"),
classNameBindings: [":user-field"],
cantMoveUp: propertyEqual("userField", "firstField"),
cantMoveDown: propertyEqual("userField", "lastField"),
userFieldsDescription: i18n("admin.user_fields.description"),
@discourseComputed("buffered.field_type")
bufferedFieldType(fieldType) {
return UserField.fieldTypeById(fieldType);
},
@on("didInsertElement")
@observes("editing")
_focusOnEdit() {
if (this.editing) {
scheduleOnce("afterRender", this, "_focusName");
}
},
_focusName() {
$(".user-field-name").select();
},
@discourseComputed("userField.field_type")
fieldName(fieldType) {
return UserField.fieldTypeById(fieldType).get("name");
},
@discourseComputed(
"userField.editable",
"userField.required",
"userField.show_on_profile",
"userField.show_on_user_card"
)
flags(editable, required, showOnProfile, showOnUserCard) {
const ret = [];
if (editable) {
ret.push(I18n.t("admin.user_fields.editable.enabled"));
}
if (required) {
ret.push(I18n.t("admin.user_fields.required.enabled"));
}
if (showOnProfile) {
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
}
if (showOnUserCard) {
ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
}
return ret.join(", ");
},
actions: {
save() {
const buffered = this.buffered;
const attrs = buffered.getProperties(
"name",
"description",
"field_type",
"editable",
"required",
"show_on_profile",
"show_on_user_card",
"options"
);
this.userField
.save(attrs)
.then(() => {
this.set("editing", false);
this.commitBuffer();
})
.catch(popupAjaxError);
},
edit() {
this.set("editing", true);
},
cancel() {
const id = this.get("userField.id");
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.rollbackBuffer();
this.set("editing", false);
}
},
},
});
@@ -0,0 +1,30 @@
import I18n from "I18n";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import bootbox from "bootbox";
export default Component.extend({
classNames: ["watched-word"],
watchedWord: null,
xIcon: iconHTML("times").htmlSafe(),
init() {
this._super(...arguments);
this.set("watchedWord", this.get("word.word"));
},
click() {
this.word
.destroy()
.then(() => {
this.action(this.word);
})
.catch((e) => {
bootbox.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`,
})
);
});
},
});
@@ -0,0 +1,47 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
classNames: ["hook-event"],
typeName: alias("type.name"),
@discourseComputed("typeName")
name(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
},
@discourseComputed("typeName")
details(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
},
@discourseComputed("model.[]", "typeName")
eventTypeExists(eventTypes, typeName) {
return eventTypes.any((event) => event.name === typeName);
},
@discourseComputed("eventTypeExists")
enabled: {
get(eventTypeExists) {
return eventTypeExists;
},
set(value, eventTypeExists) {
const type = this.type;
const model = this.model;
// add an association when not exists
if (value !== eventTypeExists) {
if (value) {
model.addObject(type);
} else {
model.removeObjects(
model.filter((eventType) => eventType.name === type.name)
);
}
}
return value;
},
},
});
@@ -0,0 +1,113 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
import bootbox from "bootbox";
export default Component.extend({
tagName: "li",
expandDetails: null,
expandDetailsRequestKey: "request",
expandDetailsResponseKey: "response",
@discourseComputed("model.status")
statusColorClasses(status) {
if (!status) {
return "";
}
if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
},
@discourseComputed("model.created_at")
createdAt(createdAt) {
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
},
@discourseComputed("model.duration")
completion(duration) {
const seconds = Math.floor(duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
},
@discourseComputed("expandDetails")
expandRequestIcon(expandDetails) {
return expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
},
@discourseComputed("expandDetails")
expandResponseIcon(expandDetails) {
return expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
},
actions: {
redeliver() {
return bootbox.confirm(
I18n.t("admin.web_hooks.events.redeliver_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
ajax(
`/admin/api/web_hooks/${this.get(
"model.web_hook_id"
)}/events/${this.get("model.id")}/redeliver`,
{ type: "POST" }
)
.then((json) => {
this.set("model", json.web_hook_event);
})
.catch(popupAjaxError);
}
}
);
},
toggleRequest() {
const expandDetailsKey = this.expandDetailsRequestKey;
if (this.expandDetails !== expandDetailsKey) {
let headers = Object.assign(
{
"Request URL": this.get("model.request_url"),
"Request method": "POST",
},
ensureJSON(this.get("model.headers"))
);
this.setProperties({
headers: plainJSON(headers),
body: prettyJSON(this.get("model.payload")),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.payload"),
});
} else {
this.set("expandDetails", null);
}
},
toggleResponse() {
const expandDetailsKey = this.expandDetailsResponseKey;
if (this.expandDetails !== expandDetailsKey) {
this.setProperties({
headers: plainJSON(this.get("model.response_headers")),
body: this.get("model.response_body"),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.body"),
});
} else {
this.set("expandDetails", null);
}
},
},
});
@@ -0,0 +1,38 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["far-circle", "times-circle", "circle", "circle"],
circleIcon: null,
deliveryStatus: null,
@discourseComputed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find((s) => s.id === lastDeliveryStatus);
},
@discourseComputed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},
@discourseComputed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},
didReceiveAttrs() {
this._super(...arguments);
this.set(
"circleIcon",
iconHTML(this.icon, { class: this.class }).htmlSafe()
);
this.set(
"deliveryStatus",
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
);
},
});
@@ -0,0 +1,12 @@
import Component from "@ember/component";
export default Component.extend({
didInsertElement() {
this._super(...arguments);
$("body").addClass("admin-interface");
},
willDestroyElement() {
this._super(...arguments);
$("body").removeClass("admin-interface");
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});
@@ -0,0 +1,73 @@
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { computed, action } from "@ember/object";
import loadScript, { loadCSS } from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
/**
An input field for a color.
@param hexValue is a reference to the color's hex value.
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor.
@params valid is a boolean indicating if the input field is a valid color.
**/
export default Component.extend({
classNames: ["color-picker"],
onlyHex: true,
styleSelection: true,
maxlength: computed("onlyHex", function () {
return this.onlyHex ? 6 : null;
}),
@action
onHexInput(color) {
this.attrs.onChangeColor && this.attrs.onChangeColor(color || "");
},
@observes("hexValue", "brightnessValue", "valid")
hexValueChanged: function () {
const hex = this.hexValue;
let text = this.element.querySelector("input.hex-input");
this.attrs.onChangeColor && this.attrs.onChangeColor(hex);
if (this.valid) {
this.styleSelection &&
text.setAttribute(
"style",
"color: " +
(this.brightnessValue > 125 ? "black" : "white") +
"; background-color: #" +
hex +
";"
);
if (this.pickerLoaded) {
$(this.element.querySelector(".picker")).spectrum({
color: "#" + hex,
});
}
} else {
this.styleSelection && text.setAttribute("style", "");
}
},
didInsertElement() {
loadScript("/javascripts/spectrum.js").then(() => {
loadCSS("/javascripts/spectrum.css").then(() => {
schedule("afterRender", () => {
$(this.element.querySelector(".picker"))
.spectrum({ color: "#" + this.hexValue })
.on("change.spectrum", (me, color) => {
this.set("hexValue", color.toHexString().replace("#", ""));
});
this.set("pickerLoaded", true);
});
});
});
schedule("afterRender", () => this.hexValueChanged());
},
});
@@ -0,0 +1,54 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";
import Component from "@ember/component";
import bootbox from "bootbox";
export default Component.extend({
editorId: reads("fieldName"),
@discourseComputed("fieldName")
currentEditorMode(fieldName) {
return fieldName === "css" ? "scss" : fieldName;
},
@discourseComputed("fieldName", "styles.html", "styles.css")
resetDisabled(fieldName) {
return (
this.get(`styles.${fieldName}`) ===
this.get(`styles.default_${fieldName}`)
);
},
@discourseComputed("styles", "fieldName")
editorContents: {
get(styles, fieldName) {
return styles[fieldName];
},
set(value, styles, fieldName) {
styles.setField(fieldName, value);
return value;
},
},
actions: {
reset() {
bootbox.confirm(
I18n.t("admin.customize.email_style.reset_confirm", {
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.styles.setField(
this.fieldName,
this.styles.get(`default_${this.fieldName}`)
);
this.notifyPropertyChange("editorContents");
}
}
);
},
},
});
@@ -0,0 +1,82 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { or } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { on, observes } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Category from "discourse/models/category";
import bootbox from "bootbox";
export default Component.extend(bufferedProperty("host"), {
editToggled: false,
tagName: "tr",
categoryId: null,
editing: or("host.isNew", "editToggled"),
@on("didInsertElement")
@observes("editing")
_focusOnInput() {
schedule("afterRender", () => {
this.element.querySelector(".host-name").focus();
});
},
@discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) {
return isSaving || isEmpty(host);
},
actions: {
edit() {
this.set("categoryId", this.get("host.category.id"));
this.set("editToggled", true);
},
save() {
if (this.cantSave) {
return;
}
const props = this.buffered.getProperties(
"host",
"allowed_paths",
"class_name"
);
props.category_id = this.categoryId;
const host = this.host;
host
.save(props)
.then(() => {
host.set("category", Category.findById(this.categoryId));
this.set("editToggled", false);
})
.catch(popupAjaxError);
},
delete() {
bootbox.confirm(I18n.t("admin.embedding.confirm_delete"), (result) => {
if (result) {
this.host.destroyRecord().then(() => {
this.deleteHost(this.host);
});
}
});
},
cancel() {
const host = this.host;
if (host.get("isNew")) {
this.deleteHost(host);
} else {
this.rollbackBuffer();
this.set("editToggled", false);
}
},
},
});
@@ -0,0 +1,32 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
export default Component.extend({
classNames: ["embed-setting"],
@discourseComputed("field")
inputId(field) {
return field.dasherize();
},
@discourseComputed("field")
translationKey(field) {
return `admin.embedding.${field}`;
},
@discourseComputed("type")
isCheckbox(type) {
return type === "checkbox";
},
@discourseComputed("value")
checked: {
get(value) {
return !!value;
},
set(value) {
this.set("value", value);
return value;
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["flag-user-lists"],
});
@@ -0,0 +1,11 @@
import Component from "@ember/component";
import { on, observes } from "discourse-common/utils/decorators";
import highlightSyntax from "discourse/lib/highlight-syntax";
export default Component.extend({
@on("didInsertElement")
@observes("code")
_refresh() {
highlightSyntax(this.element, this.siteSettings, this.session);
},
});
@@ -0,0 +1,42 @@
import I18n from "I18n";
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["inline-edit"],
checked: null,
checkedInternal: null,
init() {
this._super(...arguments);
this.set("checkedInternal", this.checked);
},
@observes("checked")
checkedChanged() {
this.set("checkedInternal", this.checked);
},
@discourseComputed("labelKey")
label(key) {
return I18n.t(key);
},
@discourseComputed("checked", "checkedInternal")
changed(checked, checkedInternal) {
return !!checked !== !!checkedInternal;
},
actions: {
cancelled() {
this.set("checkedInternal", this.checked);
},
finished() {
this.set("checked", this.checkedInternal);
this.action();
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["install-theme-item"],
});
@@ -0,0 +1,117 @@
import I18n from "I18n";
import EmberObject from "@ember/object";
import { later } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import AdminUser from "admin/models/admin-user";
import copyText from "discourse/lib/copy-text";
import bootbox from "bootbox";
export default Component.extend({
classNames: ["ip-lookup"],
@discourseComputed("other_accounts.length", "totalOthersWithSameIP")
otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) {
// can only delete up to 50 accounts at a time
const total = Math.min(50, totalOthersWithSameIP || 0);
const visible = Math.min(50, otherAccountsLength || 0);
return Math.max(visible, total);
},
actions: {
lookup() {
this.set("show", true);
if (!this.location) {
ajax("/admin/users/ip-info", {
data: { ip: this.ip },
}).then((location) =>
this.set("location", EmberObject.create(location))
);
}
if (!this.other_accounts) {
this.set("otherAccountsLoading", true);
const data = {
ip: this.ip,
exclude: this.userId,
order: "trust_level DESC",
};
ajax("/admin/users/total-others-with-same-ip", {
data,
}).then((result) => this.set("totalOthersWithSameIP", result.total));
AdminUser.findAll("active", data).then((users) => {
this.setProperties({
other_accounts: users,
otherAccountsLoading: false,
});
});
}
},
hide() {
this.set("show", false);
},
copy() {
let text = `IP: ${this.ip}\n`;
const location = this.location;
if (location) {
if (location.hostname) {
text += `${I18n.t("ip_lookup.hostname")}: ${location.hostname}\n`;
}
text += I18n.t("ip_lookup.location");
if (location.location) {
text += `: ${location.location}\n`;
} else {
text += `: ${I18n.t("ip_lookup.location_not_found")}\n`;
}
if (location.organization) {
text += I18n.t("ip_lookup.organisation");
text += `: ${location.organization}\n`;
}
}
const $copyRange = $('<p id="copy-range"></p>');
$copyRange.html(text.trim().replace(/\n/g, "<br>"));
$(document.body).append($copyRange);
if (copyText(text, $copyRange[0])) {
this.set("copied", true);
later(() => this.set("copied", false), 2000);
}
$copyRange.remove();
},
deleteOtherAccounts() {
bootbox.confirm(
I18n.t("ip_lookup.confirm_delete_other_accounts"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
this.setProperties({
other_accounts: null,
otherAccountsLoading: true,
totalOthersWithSameIP: null,
});
ajax("/admin/users/delete-others-with-same-ip.json", {
type: "DELETE",
data: {
ip: this.ip,
exclude: this.userId,
order: "trust_level DESC",
},
}).then(() => this.send("lookup"));
}
}
);
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});
@@ -0,0 +1,41 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
import Component from "@ember/component";
import { afterRender } from "discourse-common/utils/decorators";
const ACTIONS = ["delete", "delete_replies", "edit", "none"];
export default Component.extend({
postId: null,
postAction: null,
postEdit: null,
@discourseComputed
penaltyActions() {
return ACTIONS.map((id) => {
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
});
},
editing: equal("postAction", "edit"),
actions: {
penaltyChanged(postAction) {
this.set("postAction", postAction);
// If we switch to edit mode, jump to the edit textarea
if (postAction === "edit") {
this._focusEditTextarea();
}
},
},
@afterRender
_focusEditTextarea() {
const elem = this.element;
const body = elem.closest(".modal-body");
body.scrollTo(0, body.clientHeight);
elem.querySelector(".post-editor").focus();
},
});
@@ -0,0 +1,89 @@
import I18n from "I18n";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import Permalink from "admin/models/permalink";
import bootbox from "bootbox";
export default Component.extend({
classNames: ["permalink-form"],
formSubmitted: false,
permalinkType: "topic_id",
permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"),
@discourseComputed
permalinkTypes() {
return [
{ id: "topic_id", name: I18n.t("admin.permalink.topic_id") },
{ id: "post_id", name: I18n.t("admin.permalink.post_id") },
{ id: "category_id", name: I18n.t("admin.permalink.category_id") },
{ id: "tag_name", name: I18n.t("admin.permalink.tag_name") },
{ id: "external_url", name: I18n.t("admin.permalink.external_url") },
];
},
didInsertElement() {
this._super(...arguments);
schedule("afterRender", () => {
$(this.element.querySelector(".external-url")).keydown((e) => {
// enter key
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},
focusPermalink() {
schedule("afterRender", () =>
this.element.querySelector(".permalink-url").focus()
);
},
actions: {
submit() {
if (!this.formSubmitted) {
this.set("formSubmitted", true);
Permalink.create({
url: this.url,
permalink_type: this.permalinkType,
permalink_type_value: this.permalink_type_value,
})
.save()
.then(
(result) => {
this.setProperties({
url: "",
permalink_type_value: "",
formSubmitted: false,
});
this.action(Permalink.create(result.permalink));
this.focusPermalink();
},
(e) => {
this.set("formSubmitted", false);
let error;
if (e.responseJSON && e.responseJSON.errors) {
error = I18n.t("generic_error_with_reason", {
error: e.responseJSON.errors.join(". "),
});
} else {
error = I18n.t("generic_error");
}
bootbox.alert(error, () => this.focusPermalink());
}
);
}
},
onChangePermalinkType(type) {
this.set("permalinkType", type);
},
},
});
@@ -0,0 +1,16 @@
import { action } from "@ember/object";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
checked: false,
didReceiveAttrs() {
this._super(...arguments);
this.set("checked", !!this.filter.default);
},
@action
onChange() {
this.applyFilter(this.filter.id, !this.checked || undefined);
},
});
@@ -0,0 +1,12 @@
import { action } from "@ember/object";
import { readOnly } from "@ember/object/computed";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
category: readOnly("filter.default"),
@action
onChange(categoryId) {
this.applyFilter(this.filter.id, categoryId || undefined);
},
});
@@ -0,0 +1,9 @@
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
@action
onChange(value) {
this.applyFilter(this.filter.id, value);
},
});
@@ -0,0 +1,18 @@
import { computed } from "@ember/object";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
classNames: ["group-filter"],
@computed
get groupOptions() {
return (this.site.groups || []).map((group) => {
return { name: group["name"], value: group["id"] };
});
},
@computed("filter.default")
get groupId() {
return this.filter.default ? parseInt(this.filter.default, 10) : null;
},
});
@@ -0,0 +1,3 @@
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend();
@@ -0,0 +1,140 @@
import getURL from "discourse-common/lib/get-url";
import I18n from "I18n";
import { later, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed, { on } from "discourse-common/utils/decorators";
/*global Resumable:true */
/**
Example usage:
{{resumable-upload
target="/admin/backups/upload"
success=(action "successAction")
error=(action "errorAction")
uploadText="UPLOAD"
}}
**/
export default Component.extend({
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
isUploading: false,
progress: 0,
rerenderTriggers: ["isUploading", "progress"],
uploadingIcon: null,
progressBar: null,
@on("init")
_initialize() {
this.resumable = new Resumable({
target: getURL(this.target),
maxFiles: 1, // only 1 file at a time
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")
.content,
},
});
this.resumable.on("fileAdded", () => {
// automatically upload the selected file
this.resumable.upload();
// mark as uploading
later(() => {
this.set("isUploading", true);
this._updateIcon();
});
});
this.resumable.on("fileProgress", (file) => {
// update progress
later(() => {
this.set("progress", parseInt(file.progress() * 100, 10));
this._updateProgressBar();
});
});
this.resumable.on("fileSuccess", (file) => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to reload its model
this.success(file.fileName);
});
});
this.resumable.on("fileError", (file, message) => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to display the error message
this.error(file.fileName, message);
});
});
},
@on("didInsertElement")
_assignBrowse() {
schedule("afterRender", () => this.resumable.assignBrowse($(this.element)));
},
@on("willDestroyElement")
_teardown() {
if (this.resumable) {
this.resumable.cancel();
this.resumable = null;
}
},
@discourseComputed("title", "text")
translatedTitle(title, text) {
return title ? I18n.t(title) : text;
},
@discourseComputed("isUploading", "progress")
text(isUploading, progress) {
if (isUploading) {
return progress + " %";
} else {
return this.uploadText;
}
},
didReceiveAttrs() {
this._super(...arguments);
this._updateIcon();
},
click() {
if (this.isUploading) {
this.resumable.cancel();
later(() => this._reset());
return false;
} else {
return true;
}
},
_updateIcon() {
const icon = this.isUploading ? "times" : "upload";
this.set("uploadingIcon", `${iconHTML(icon)}`.htmlSafe());
},
_updateProgressBar() {
const pb = `${"width:" + this.progress + "%"}`.htmlSafe();
this.set("progressBar", pb);
},
_reset() {
this.setProperties({ isUploading: false, progress: 0 });
this._updateIcon();
this._updateProgressBar();
},
});
@@ -0,0 +1,94 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import bootbox from "bootbox";
/**
A form to create an IP address that will be blocked or allowed.
Example usage:
{{screened-ip-address-form action=(action "recordAdded")}}
where action is a callback on the controller or route that will get called after
the new record is successfully saved. It is called with the new ScreenedIpAddress record
as an argument.
**/
import ScreenedIpAddress from "admin/models/screened-ip-address";
import { on } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["screened-ip-address-form"],
formSubmitted: false,
actionName: "block",
@discourseComputed("siteSettings.use_admin_ip_allowlist")
actionNames(adminAllowlistEnabled) {
if (adminAllowlistEnabled) {
return [
{ id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") },
{
id: "do_nothing",
name: I18n.t("admin.logs.screened_ips.actions.do_nothing"),
},
{
id: "allow_admin",
name: I18n.t("admin.logs.screened_ips.actions.allow_admin"),
},
];
} else {
return [
{ id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") },
{
id: "do_nothing",
name: I18n.t("admin.logs.screened_ips.actions.do_nothing"),
},
];
}
},
actions: {
submit() {
if (!this.formSubmitted) {
this.set("formSubmitted", true);
const screenedIpAddress = ScreenedIpAddress.create({
ip_address: this.ip_address,
action_name: this.actionName,
});
screenedIpAddress
.save()
.then((result) => {
this.setProperties({ ip_address: "", formSubmitted: false });
this.action(ScreenedIpAddress.create(result.screened_ip_address));
schedule("afterRender", () =>
this.element.querySelector(".ip-address-input").focus()
);
})
.catch((e) => {
this.set("formSubmitted", false);
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg, () =>
this.element.querySelector(".ip-address-input").focus()
);
});
}
},
},
@on("didInsertElement")
_init() {
schedule("afterRender", () => {
$(this.element.querySelector(".ip-address-input")).keydown((e) => {
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},
});
@@ -0,0 +1,111 @@
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
import { set } from "@ember/object";
export default Component.extend({
classNameBindings: [":value-list", ":secret-value-list"],
inputDelimiter: null,
collection: null,
values: null,
validationMessage: null,
@on("didReceiveAttrs")
_setupCollection() {
const values = this.values;
this.set(
"collection",
this._splitValues(values, this.inputDelimiter || "\n")
);
},
actions: {
changeKey(index, newValue) {
if (this._checkInvalidInput(newValue)) {
return;
}
this._replaceValue(index, newValue, "key");
},
changeSecret(index, newValue) {
if (this._checkInvalidInput(newValue)) {
return;
}
this._replaceValue(index, newValue, "secret");
},
addValue() {
if (this._checkInvalidInput([this.newKey, this.newSecret])) {
return;
}
this._addValue(this.newKey, this.newSecret);
this.setProperties({ newKey: "", newSecret: "" });
},
removeValue(value) {
this._removeValue(value);
},
},
_checkInvalidInput(inputs) {
this.set("validationMessage", null);
for (let input of inputs) {
if (isEmpty(input) || input.includes("|")) {
this.set(
"validationMessage",
I18n.t("admin.site_settings.secret_list.invalid_input")
);
return true;
}
}
},
_addValue(value, secret) {
this.collection.addObject({ key: value, secret: secret });
this._saveValues();
},
_removeValue(value) {
const collection = this.collection;
collection.removeObject(value);
this._saveValues();
},
_replaceValue(index, newValue, keyName) {
let item = this.collection[index];
set(item, keyName, newValue);
this._saveValues();
},
_saveValues() {
this.set(
"values",
this.collection
.map(function (elem) {
return `${elem.key}|${elem.secret}`;
})
.join("\n")
);
},
_splitValues(values, delimiter) {
if (values && values.length) {
const keys = ["key", "secret"];
var res = [];
values.split(delimiter).forEach(function (str) {
var object = {};
str.split("|").forEach(function (a, i) {
object[keys[i]] = a;
});
res.push(object);
});
return res;
} else {
return [];
}
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});
@@ -0,0 +1,59 @@
import { empty } from "@ember/object/computed";
import Component from "@ember/component";
import { action } from "@ember/object";
import { on } from "discourse-common/utils/decorators";
export default Component.extend({
classNameBindings: [":simple-list", ":value-list"],
inputEmpty: empty("newValue"),
inputDelimiter: null,
newValue: "",
collection: null,
values: null,
@on("didReceiveAttrs")
_setupCollection() {
this.set("collection", this._splitValues(this.values, this.inputDelimiter));
},
keyDown(event) {
if (event.which === 13) {
this.addValue(this.newValue);
return;
}
},
@action
changeValue(index, newValue) {
this.collection.replace(index, 1, [newValue]);
this.collection.arrayContentDidChange(index);
this._onChange();
},
@action
addValue(newValue) {
if (this.inputEmpty) {
return;
}
this.set("newValue", null);
this.collection.addObject(newValue);
this._onChange();
},
@action
removeValue(value) {
this.collection.removeObject(value);
this._onChange();
},
_onChange() {
this.attrs.onChange && this.attrs.onChange(this.collection);
},
_splitValues(values, delimiter) {
return values && values.length
? values.split(delimiter || "\n").filter(Boolean)
: [];
},
});
@@ -0,0 +1,15 @@
import Component from "@ember/component";
import BufferedContent from "discourse/mixins/buffered-content";
import SiteSetting from "admin/models/site-setting";
import SettingComponent from "admin/mixins/setting-component";
export default Component.extend(BufferedContent, SettingComponent, {
updateExistingUsers: null,
_save() {
const setting = this.buffered;
return SiteSetting.update(setting.get("setting"), setting.get("value"), {
updateExistingUsers: this.updateExistingUsers,
});
},
});
@@ -0,0 +1,6 @@
import ImageUploader from "discourse/components/image-uploader";
export default ImageUploader.extend({
layoutName: "components/image-uploader",
uploadUrlParams: "&for_site_setting=true",
});
@@ -0,0 +1,19 @@
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import Component from "@ember/component";
export default Component.extend({
@discourseComputed("value")
enabled: {
get(value) {
if (isEmpty(value)) {
return false;
}
return value.toString() === "true";
},
set(value) {
this.set("value", value ? "true" : "false");
return value;
},
},
});
@@ -0,0 +1,15 @@
import Component from "@ember/component";
import Category from "discourse/models/category";
import { computed } from "@ember/object";
export default Component.extend({
selectedCategories: computed("value", function () {
return Category.findByIds(this.value.split("|").filter(Boolean));
}),
actions: {
onChangeSelectedCategories(value) {
this.set("value", (value || []).mapBy("id").join("|"));
},
},
});
@@ -0,0 +1,52 @@
import Component from "@ember/component";
import { computed, action } from "@ember/object";
function RGBToHex(rgb) {
// Choose correct separator
let sep = rgb.indexOf(",") > -1 ? "," : " ";
// Turn "rgb(r,g,b)" into [r,g,b]
rgb = rgb.substr(4).split(")")[0].split(sep);
let r = (+rgb[0]).toString(16),
g = (+rgb[1]).toString(16),
b = (+rgb[2]).toString(16);
if (r.length === 1) {
r = "0" + r;
}
if (g.length === 1) {
g = "0" + g;
}
if (b.length === 1) {
b = "0" + b;
}
return "#" + r + g + b;
}
export default Component.extend({
valid: computed("value", function () {
let value = this.value.toLowerCase();
let testColor = new Option().style;
testColor.color = value;
if (!testColor.color && !value.startsWith("#")) {
value = `#${value}`;
testColor = new Option().style;
testColor.color = value;
}
let hexifiedColor = RGBToHex(testColor.color);
if (hexifiedColor.includes("NaN")) {
hexifiedColor = testColor.color;
}
return testColor.color && hexifiedColor === value;
}),
@action
onChangeColor(color) {
this.set("value", color);
},
});
@@ -0,0 +1,40 @@
import Component from "@ember/component";
import { computed } from "@ember/object";
import { makeArray } from "discourse-common/lib/helpers";
export default Component.extend({
tokenSeparator: "|",
createdChoices: null,
settingValue: computed("value", function () {
return this.value.toString().split(this.tokenSeparator).filter(Boolean);
}),
settingChoices: computed(
"settingValue",
"setting.choices.[]",
"createdChoices.[]",
function () {
return [
...new Set([
...makeArray(this.settingValue),
...makeArray(this.setting.choices),
...makeArray(this.createdChoices),
]),
];
}
),
actions: {
onChangeListSetting(value) {
this.set("value", value.join(this.tokenSeparator));
},
onChangeChoices(choices) {
this.set("createdChoices", [
...new Set([...makeArray(this.createdChoices), ...makeArray(choices)]),
]);
},
},
});
@@ -0,0 +1,25 @@
import { computed } from "@ember/object";
import Component from "@ember/component";
export default Component.extend({
tokenSeparator: "|",
nameProperty: "name",
valueProperty: "id",
groupChoices: computed("site.groups", function () {
return (this.site.groups || []).map((g) => {
return { name: g.name, id: g.id.toString() };
});
}),
settingValue: computed("value", function () {
return (this.value || "").split(this.tokenSeparator).filter(Boolean);
}),
actions: {
onChangeGroupListSetting(value) {
this.set("value", value.join(this.tokenSeparator));
},
},
});
@@ -0,0 +1,11 @@
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
inputDelimiter: "|",
@action
onChange(value) {
this.set("value", value.join(this.inputDelimiter || "\n"));
},
});
@@ -0,0 +1,17 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
@discourseComputed("value")
selectedTags: {
get(value) {
return value.split("|").filter(Boolean);
},
},
@action
changeSelectedTags(tags) {
this.set("value", tags.join("|"));
},
});
@@ -0,0 +1,16 @@
import Component from "@ember/component";
import showModal from "discourse/lib/show-modal";
export default Component.extend({
actions: {
showUploadModal({ value, setting }) {
showModal("admin-uploaded-image-list", {
admin: true,
title: `admin.site_settings.${setting.setting}.title`,
model: { value, setting },
}).setProperties({
save: (v) => this.set("value", v),
});
},
},
});
@@ -0,0 +1,42 @@
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
import highlightHTML from "discourse/lib/highlight-html";
export default Component.extend({
classNames: ["site-text"],
classNameBindings: ["siteText.overridden"],
@on("didInsertElement")
highlightTerm() {
const term = this._searchTerm();
if (term) {
highlightHTML(
this.element.querySelector(".site-text-id, .site-text-value"),
term,
{
className: "text-highlight",
}
);
}
$(this.element.querySelector(".site-text-value")).ellipsis();
},
click() {
this.editAction(this.siteText);
},
_searchTerm() {
const regex = this.searchRegex;
const siteText = this.siteText;
if (regex && siteText) {
const matches = siteText.value.match(new RegExp(regex, "i"));
if (matches) {
return matches[0];
}
}
return this.term;
},
});
@@ -0,0 +1,39 @@
import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";
export default Component.extend({
classNames: ["table", "staff-actions"],
willDestroyElement() {
$(this.element).off("click.discourse-staff-logs");
},
didInsertElement() {
this._super(...arguments);
$(this.element).on(
"click.discourse-staff-logs",
"[data-link-post-id]",
(e) => {
let postId = $(e.target).attr("data-link-post-id");
this.store.find("post", postId).then((p) => {
DiscourseURL.routeTo(p.get("url"));
});
return false;
}
);
$(this.element).on(
"click.discourse-staff-logs",
"[data-link-topic-id]",
(e) => {
let topicId = $(e.target).attr("data-link-topic-id");
DiscourseURL.routeTo(`/t/${topicId}`);
return false;
}
);
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});
@@ -0,0 +1,23 @@
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import UploadMixin from "discourse/mixins/upload";
import bootbox from "bootbox";
export default Component.extend(UploadMixin, {
type: "csv",
uploadUrl: "/tags/upload",
addDisabled: alias("uploading"),
elementId: "tag-uploader",
validateUploadedFilesOptions() {
return { csvOnly: true };
},
uploadDone() {
bootbox.alert(I18n.t("tagging.upload_successful"), () => {
this.refresh();
this.closeModal();
});
},
});
@@ -0,0 +1,20 @@
import Component from "@ember/component";
import BufferedContent from "discourse/mixins/buffered-content";
import SettingComponent from "admin/mixins/setting-component";
import { ajax } from "discourse/lib/ajax";
import { url } from "discourse/lib/computed";
export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
updateUrl: url("model.id", "/admin/themes/%@/setting"),
_save() {
return ajax(this.updateUrl, {
type: "PUT",
data: {
name: this.setting.setting,
value: this.get("buffered.value"),
},
});
},
});
@@ -0,0 +1,27 @@
import Component from "@ember/component";
import BufferedContent from "discourse/mixins/buffered-content";
import SettingComponent from "admin/mixins/setting-component";
export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
_save() {
return this.model
.save({ [this.setting.setting]: this.convertNamesToIds() })
.then(() => this.store.findAll("theme"));
},
convertNamesToIds() {
return this.get("buffered.value")
.split("|")
.filter(Boolean)
.map((themeName) => {
if (themeName !== "") {
return this.setting.allThemes.find(
(theme) => theme.name === themeName
).id;
}
return themeName;
});
},
});
@@ -0,0 +1,18 @@
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import BufferedContent from "discourse/mixins/buffered-content";
import SettingComponent from "admin/mixins/setting-component";
export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
setting: alias("translation"),
type: "string",
settingName: alias("translation.key"),
_save() {
return this.model.saveTranslation(
this.get("translation.key"),
this.get("buffered.value")
);
},
});
@@ -0,0 +1,147 @@
import { gt, and } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library";
import { escape } from "pretty-text/sanitizer";
import { isTesting } from "discourse-common/config/environment";
const MAX_COMPONENTS = 4;
export default Component.extend({
childrenExpanded: false,
classNames: ["themes-list-item"],
classNameBindings: ["theme.selected:selected"],
hasComponents: gt("children.length", 0),
displayComponents: and("hasComponents", "theme.isActive"),
displayHasMore: gt("theme.childThemes.length", MAX_COMPONENTS),
click(e) {
if (!$(e.target).hasClass("others-count")) {
this.navigateToTheme();
}
},
init() {
this._super(...arguments);
this.scheduleAnimation();
},
@observes("theme.selected")
triggerAnimation() {
this.animate();
},
scheduleAnimation() {
schedule("afterRender", () => {
this.animate(true);
});
},
animate(isInitial) {
const $container = $(this.element);
const $list = $(this.element.querySelector(".components-list"));
if ($list.length === 0 || isTesting()) {
return;
}
const duration = 300;
if (this.get("theme.selected")) {
this.collapseComponentsList($container, $list, duration);
} else if (!isInitial) {
this.expandComponentsList($container, $list, duration);
}
},
@discourseComputed(
"theme.component",
"theme.childThemes.@each.name",
"theme.childThemes.length",
"childrenExpanded"
)
children() {
const theme = this.theme;
let children = theme.get("childThemes");
if (theme.get("component") || !children) {
return [];
}
children = this.childrenExpanded
? children
: children.slice(0, MAX_COMPONENTS);
return children.map((t) => {
const name = escape(t.name);
return t.enabled ? name : `${iconHTML("ban")} ${name}`;
});
},
@discourseComputed("children")
childrenString(children) {
return children.join(", ");
},
@discourseComputed(
"theme.childThemes.length",
"theme.component",
"childrenExpanded",
"children.length"
)
moreCount(childrenCount, component, expanded) {
if (component || !childrenCount || expanded) {
return 0;
}
return childrenCount - MAX_COMPONENTS;
},
expandComponentsList($container, $list, duration) {
$container.css("height", `${$container.height()}px`);
$list.css("display", "");
$container.animate(
{
height: `${$container.height() + $list.outerHeight(true)}px`,
},
{
duration,
done: () => {
$list.css("display", "");
$container.css("height", "");
},
}
);
$list.animate(
{
opacity: 1,
},
{
duration,
}
);
},
collapseComponentsList($container, $list, duration) {
$container.animate(
{
height: `${$container.height() - $list.outerHeight(true)}px`,
},
{
duration,
done: () => {
$list.css("display", "none");
$container.css("height", "");
},
}
);
$list.animate(
{
opacity: 0,
},
{
duration,
}
);
},
actions: {
toggleChildrenExpanded() {
this.toggleProperty("childrenExpanded");
},
},
});
@@ -0,0 +1,81 @@
import { gt, equal } from "@ember/object/computed";
import Component from "@ember/component";
import { THEMES, COMPONENTS } from "admin/models/theme";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Component.extend({
router: service(),
THEMES,
COMPONENTS,
classNames: ["themes-list"],
hasThemes: gt("themesList.length", 0),
hasActiveThemes: gt("activeThemes.length", 0),
hasInactiveThemes: gt("inactiveThemes.length", 0),
themesTabActive: equal("currentTab", THEMES),
componentsTabActive: equal("currentTab", COMPONENTS),
@discourseComputed("themes", "components", "currentTab")
themesList(themes, components) {
if (this.themesTabActive) {
return themes;
} else {
return components;
}
},
@discourseComputed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
"themesList.@each.default"
)
inactiveThemes(themes) {
if (this.componentsTabActive) {
return themes.filter((theme) => theme.get("parent_themes.length") <= 0);
}
return themes.filter(
(theme) => !theme.get("user_selectable") && !theme.get("default")
);
},
@discourseComputed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
"themesList.@each.default"
)
activeThemes(themes) {
if (this.componentsTabActive) {
return themes.filter((theme) => theme.get("parent_themes.length") > 0);
} else {
return themes
.filter((theme) => theme.get("user_selectable") || theme.get("default"))
.sort((a, b) => {
if (a.get("default") && !b.get("default")) {
return -1;
} else if (b.get("default")) {
return 1;
}
return a
.get("name")
.toLowerCase()
.localeCompare(b.get("name").toLowerCase());
});
}
},
actions: {
changeView(newTab) {
if (newTab !== this.currentTab) {
this.set("currentTab", newTab);
}
},
navigateToTheme(theme) {
this.router.transitionTo("adminCustomizeThemes.show", theme);
},
},
});
@@ -0,0 +1,110 @@
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { empty, reads } from "@ember/object/computed";
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
export default Component.extend({
classNameBindings: [":value-list"],
inputInvalid: empty("newValue"),
inputDelimiter: null,
inputType: null,
newValue: "",
collection: null,
values: null,
noneKey: reads("addKey"),
@on("didReceiveAttrs")
_setupCollection() {
const values = this.values;
if (this.inputType === "array") {
this.set("collection", values || []);
return;
}
this.set(
"collection",
this._splitValues(values, this.inputDelimiter || "\n")
);
},
@discourseComputed("choices.[]", "collection.[]")
filteredChoices(choices, collection) {
return makeArray(choices).filter((i) => collection.indexOf(i) < 0);
},
keyDown(event) {
if (event.keyCode === 13) {
this.send("addValue", this.newValue);
}
},
actions: {
changeValue(index, newValue) {
this._replaceValue(index, newValue);
},
addValue(newValue) {
if (this.inputInvalid) {
return;
}
this.set("newValue", null);
this._addValue(newValue);
},
removeValue(value) {
this._removeValue(value);
},
selectChoice(choice) {
this._addValue(choice);
},
},
_addValue(value) {
this.collection.addObject(value);
if (this.choices) {
this.set("choices", this.choices.rejectBy("id", value));
} else {
this.set("choices", []);
}
this._saveValues();
},
_removeValue(value) {
this.collection.removeObject(value);
if (this.choices) {
this.set("choices", this.choices.concat([value]).uniq());
} else {
this.set("choices", makeArray(value));
}
this._saveValues();
},
_replaceValue(index, newValue) {
this.collection.replace(index, 1, [newValue]);
this._saveValues();
},
_saveValues() {
if (this.inputType === "array") {
this.set("values", this.collection);
return;
}
this.set("values", this.collection.join(this.inputDelimiter || "\n"));
},
_splitValues(values, delimiter) {
if (values && values.length) {
return values.split(delimiter).filter((x) => x);
} else {
return [];
}
},
});
@@ -0,0 +1,102 @@
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import WatchedWord from "admin/models/watched-word";
import bootbox from "bootbox";
import discourseComputed, {
on,
observes,
} from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["watched-word-form"],
formSubmitted: false,
actionKey: null,
showMessage: false,
@discourseComputed("regularExpressions")
placeholderKey(regularExpressions) {
return (
"admin.watched_words.form.placeholder" +
(regularExpressions ? "_regexp" : "")
);
},
@observes("word")
removeMessage() {
if (this.showMessage && !isEmpty(this.word)) {
this.set("showMessage", false);
}
},
@discourseComputed("word")
isUniqueWord(word) {
const words = this.filteredContent || [];
const filtered = words.filter(
(content) => content.action === this.actionKey
);
return filtered.every(
(content) => content.word.toLowerCase() !== word.toLowerCase()
);
},
actions: {
submit() {
if (!this.isUniqueWord) {
this.setProperties({
showMessage: true,
message: I18n.t("admin.watched_words.form.exists"),
});
return;
}
if (!this.formSubmitted) {
this.set("formSubmitted", true);
const watchedWord = WatchedWord.create({
word: this.word,
action: this.actionKey,
});
watchedWord
.save()
.then((result) => {
this.setProperties({
word: "",
formSubmitted: false,
showMessage: true,
message: I18n.t("admin.watched_words.form.success"),
});
this.action(WatchedWord.create(result));
schedule("afterRender", () =>
this.element.querySelector(".watched-word-input").focus()
);
})
.catch((e) => {
this.set("formSubmitted", false);
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg, () =>
this.element.querySelector(".watched-word-input").focus()
);
});
}
},
},
@on("didInsertElement")
_init() {
schedule("afterRender", () => {
$(this.element.querySelector(".watched-word-input")).keydown((e) => {
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},
});
@@ -0,0 +1,29 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import UploadMixin from "discourse/mixins/upload";
import bootbox from "bootbox";
export default Component.extend(UploadMixin, {
type: "txt",
classNames: "watched-words-uploader",
uploadUrl: "/admin/logs/watched_words/upload",
addDisabled: alias("uploading"),
validateUploadedFilesOptions() {
return { skipValidation: true };
},
@discourseComputed("actionKey")
data(actionKey) {
return { action_key: actionKey };
},
uploadDone() {
if (this) {
bootbox.alert(I18n.t("admin.watched_words.form.upload_successful"));
this.done();
}
},
});
@@ -0,0 +1,14 @@
import { popupAjaxError } from "discourse/lib/ajax-error";
import Controller from "@ember/controller";
export default Controller.extend({
actions: {
revokeKey(key) {
key.revoke().catch(popupAjaxError);
},
undoRevokeKey(key) {
key.undoRevoke().catch(popupAjaxError);
},
},
});
@@ -0,0 +1,67 @@
import I18n from "I18n";
import { isBlank } from "@ember/utils";
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
export default Controller.extend({
userModes: [
{ id: "all", name: I18n.t("admin.api.all_users") },
{ id: "single", name: I18n.t("admin.api.single_user") },
],
useGlobalKey: false,
scopes: null,
@discourseComputed("userMode")
showUserSelector(mode) {
return mode === "single";
},
@discourseComputed("model.description", "model.username", "userMode")
saveDisabled(description, username, userMode) {
if (isBlank(description)) {
return true;
}
if (userMode === "single" && isBlank(username)) {
return true;
}
return false;
},
actions: {
changeUserMode(value) {
if (value === "all") {
this.model.set("username", null);
}
this.set("userMode", value);
},
save() {
if (!this.useGlobalKey) {
const selectedScopes = Object.values(this.scopes)
.flat()
.filter((action) => {
return action.selected;
});
this.model.set("scopes", selectedScopes);
}
this.model.save().catch(popupAjaxError);
},
continue() {
this.transitionToRoute("adminApiKeys.show", this.model.id);
},
showURLs(urls) {
return showModal("admin-api-key-urls", {
admin: true,
model: {
urls,
},
});
},
},
});
@@ -0,0 +1,66 @@
import { bufferedProperty } from "discourse/mixins/buffered-content";
import Controller from "@ember/controller";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { empty } from "@ember/object/computed";
import showModal from "discourse/lib/show-modal";
export default Controller.extend(bufferedProperty("model"), {
isNew: empty("model.id"),
actions: {
saveDescription() {
const buffered = this.buffered;
const attrs = buffered.getProperties("description");
this.model
.save(attrs)
.then(() => {
this.set("editingDescription", false);
this.rollbackBuffer();
})
.catch(popupAjaxError);
},
cancel() {
const id = this.get("userField.id");
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.rollbackBuffer();
this.set("editing", false);
}
},
editDescription() {
this.toggleProperty("editingDescription");
if (!this.editingDescription) {
this.rollbackBuffer();
}
},
revokeKey(key) {
key.revoke().catch(popupAjaxError);
},
deleteKey(key) {
key
.destroyRecord()
.then(() => this.transitionToRoute("adminApiKeys.index"))
.catch(popupAjaxError);
},
undoRevokeKey(key) {
key.undoRevoke().catch(popupAjaxError);
},
showURLs(urls) {
return showModal("admin-api-key-urls", {
admin: true,
model: {
urls,
},
});
},
},
});
@@ -0,0 +1,60 @@
import I18n from "I18n";
import { alias, equal } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { setting, i18n } from "discourse/lib/computed";
import bootbox from "bootbox";
export default Controller.extend({
adminBackups: controller(),
status: alias("adminBackups.model"),
uploadLabel: i18n("admin.backups.upload.label"),
backupLocation: setting("backup_location"),
localBackupStorage: equal("backupLocation", "local"),
@discourseComputed("status.allowRestore", "status.isOperationRunning")
restoreTitle(allowRestore, isOperationRunning) {
if (!allowRestore) {
return "admin.backups.operations.restore.is_disabled";
} else if (isOperationRunning) {
return "admin.backups.operations.is_running";
} else {
return "admin.backups.operations.restore.title";
}
},
actions: {
toggleReadOnlyMode() {
if (!this.site.get("isReadOnly")) {
bootbox.confirm(
I18n.t("admin.backups.read_only.enable.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
this.set("currentUser.hideReadOnlyAlert", true);
this._toggleReadOnlyMode(true);
}
}
);
} else {
this._toggleReadOnlyMode(false);
}
},
download(backup) {
const link = backup.get("filename");
ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() =>
bootbox.alert(I18n.t("admin.backups.operations.download.alert"))
);
},
},
_toggleReadOnlyMode(enable) {
ajax("/admin/backups/readonly", {
type: "PUT",
data: { enable },
}).then(() => this.site.set("isReadOnly", enable));
},
});
@@ -0,0 +1,13 @@
import { alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
export default Controller.extend({
adminBackups: controller(),
status: alias("adminBackups.model"),
init() {
this._super(...arguments);
this.logs = [];
},
});
@@ -0,0 +1,11 @@
import { not, and } from "@ember/object/computed";
import Controller from "@ember/controller";
export default Controller.extend({
noOperationIsRunning: not("model.isOperationRunning"),
rollbackEnabled: and(
"model.canRollback",
"model.restoreEnabled",
"noOperationIsRunning"
),
rollbackDisabled: not("rollbackEnabled"),
});
@@ -0,0 +1,39 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import bootbox from "bootbox";
export default Controller.extend({
saving: false,
replaceBadgeOwners: false,
actions: {
massAward() {
const file = document.querySelector("#massAwardCSVUpload").files[0];
if (this.model && file) {
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData(),
};
options.data.append("file", file);
options.data.append("replace_badge_owners", this.replaceBadgeOwners);
this.set("saving", true);
ajax(`/admin/badges/award/${this.model.id}`, options)
.then(() => {
bootbox.alert(I18n.t("admin.badges.mass_award.success"));
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
} else {
bootbox.alert(I18n.t("admin.badges.mass_award.aborted"));
}
},
},
});
@@ -0,0 +1,168 @@
import I18n from "I18n";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { propertyNotEqual } from "discourse/lib/computed";
import { run } from "@ember/runloop";
import bootbox from "bootbox";
export default Controller.extend(bufferedProperty("model"), {
adminBadges: controller(),
saving: false,
savingStatus: "",
badgeTypes: reads("adminBadges.badgeTypes"),
badgeGroupings: reads("adminBadges.badgeGroupings"),
badgeTriggers: reads("adminBadges.badgeTriggers"),
protectedSystemFields: reads("adminBadges.protectedSystemFields"),
readOnly: reads("buffered.system"),
showDisplayName: propertyNotEqual("name", "displayName"),
init() {
this._super(...arguments);
// this is needed because the model doesnt have default values
// and as we are using a bufferedProperty it's not accessible
// in any other way
run.next(() => {
if (this.model) {
if (!this.model.badge_type_id) {
this.model.set(
"badge_type_id",
this.get("badgeTypes.firstObject.id")
);
}
if (!this.model.badge_grouping_id) {
this.model.set(
"badge_grouping_id",
this.get("badgeGroupings.firstObject.id")
);
}
if (!this.model.trigger) {
this.model.set("trigger", this.get("badgeTriggers.firstObject.id"));
}
}
});
},
@discourseComputed("model.query", "buffered.query")
hasQuery(modelQuery, bufferedQuery) {
if (bufferedQuery) {
return bufferedQuery.trim().length > 0;
}
return modelQuery && modelQuery.trim().length > 0;
},
@observes("model.id")
_resetSaving: function () {
this.set("saving", false);
this.set("savingStatus", "");
},
actions: {
save() {
if (!this.saving) {
let fields = [
"allow_title",
"multiple_grant",
"listable",
"auto_revoke",
"enabled",
"show_posts",
"target_posts",
"name",
"description",
"long_description",
"icon",
"image",
"query",
"badge_grouping_id",
"trigger",
"badge_type_id",
];
if (this.get("buffered.system")) {
let protectedFields = this.protectedSystemFields || [];
fields = fields.filter((f) => !protectedFields.includes(f));
}
this.set("saving", true);
this.set("savingStatus", I18n.t("saving"));
const boolFields = [
"allow_title",
"multiple_grant",
"listable",
"auto_revoke",
"enabled",
"show_posts",
"target_posts",
];
const data = {};
const buffered = this.buffered;
fields.forEach(function (field) {
var d = buffered.get(field);
if (boolFields.includes(field)) {
d = !!d;
}
data[field] = d;
});
const newBadge = !this.id;
const model = this.model;
this.model
.save(data)
.then(() => {
if (newBadge) {
const adminBadges = this.get("adminBadges.model");
if (!adminBadges.includes(model)) {
adminBadges.pushObject(model);
}
this.transitionToRoute("adminBadges.show", model.get("id"));
} else {
this.commitBuffer();
this.set("savingStatus", I18n.t("saved"));
}
})
.catch(popupAjaxError)
.finally(() => {
this.set("saving", false);
this.set("savingStatus", "");
});
}
},
destroy() {
const adminBadges = this.get("adminBadges.model");
const model = this.model;
if (!model.get("id")) {
this.transitionToRoute("adminBadges.index");
return;
}
return bootbox.confirm(
I18n.t("admin.badges.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
model
.destroy()
.then(() => {
adminBadges.removeObject(model);
this.transitionToRoute("adminBadges.index");
})
.catch(() => {
bootbox.alert(I18n.t("generic_error"));
});
}
}
);
},
},
});
@@ -0,0 +1,18 @@
import Controller from "@ember/controller";
import { inject as service } from "@ember/service";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
routing: service("-routing"),
@discourseComputed("routing.currentRouteName")
selectedRoute() {
const currentRoute = this.routing.currentRouteName;
const indexRoute = "adminBadges.index";
if (currentRoute === indexRoute) {
return "adminBadges.show";
} else {
return this.routing.currentRouteName;
}
},
});
@@ -0,0 +1,96 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { later } from "@ember/runloop";
import Controller from "@ember/controller";
import bootbox from "bootbox";
export default Controller.extend({
@discourseComputed("model.colors", "onlyOverridden")
colors(allColors, onlyOverridden) {
if (onlyOverridden) {
return allColors.filter((color) => color.get("overridden"));
} else {
return allColors;
}
},
actions: {
revert: function (color) {
color.revert();
},
undo: function (color) {
color.undo();
},
copyToClipboard() {
$(".table.colors").hide();
let area = $("<textarea id='copy-range'></textarea>");
$(".table.colors").after(area);
area.text(this.model.schemeJson());
let range = document.createRange();
range.selectNode(area[0]);
window.getSelection().addRange(range);
let successful = document.execCommand("copy");
if (successful) {
this.set(
"model.savingStatus",
I18n.t("admin.customize.copied_to_clipboard")
);
} else {
this.set(
"model.savingStatus",
I18n.t("admin.customize.copy_to_clipboard_error")
);
}
later(() => {
this.set("model.savingStatus", null);
}, 2000);
window.getSelection().removeAllRanges();
$(".table.colors").show();
$(area).remove();
},
copy() {
const newColorScheme = this.model.copy();
newColorScheme.set(
"name",
I18n.t("admin.customize.colors.copy_name_prefix") +
" " +
this.get("model.name")
);
newColorScheme.save().then(() => {
this.allColors.pushObject(newColorScheme);
this.replaceRoute("adminCustomize.colors.show", newColorScheme);
});
},
save: function () {
this.model.save();
},
applyUserSelectable() {
this.model.updateUserSelectable(this.get("model.user_selectable"));
},
destroy: function () {
const model = this.model;
return bootbox.confirm(
I18n.t("admin.customize.colors.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
model.destroy().then(() => {
this.allColors.removeObject(model);
this.replaceRoute("adminCustomize.colors");
});
}
}
);
},
},
});
@@ -0,0 +1,49 @@
import I18n from "I18n";
import EmberObject from "@ember/object";
import Controller from "@ember/controller";
import showModal from "discourse/lib/show-modal";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
@discourseComputed("model.@each.id")
baseColorScheme() {
return this.model.findBy("is_base", true);
},
@discourseComputed("model.@each.id")
baseColorSchemes() {
return this.model.filterBy("is_base", true);
},
@discourseComputed("baseColorScheme")
baseColors(baseColorScheme) {
const baseColorsHash = EmberObject.create({});
baseColorScheme.get("colors").forEach((color) => {
baseColorsHash.set(color.get("name"), color);
});
return baseColorsHash;
},
actions: {
newColorSchemeWithBase(baseKey) {
const base = this.baseColorSchemes.findBy("base_scheme_id", baseKey);
const newColorScheme = base.copy();
newColorScheme.setProperties({
name: I18n.t("admin.customize.colors.new_name"),
base_scheme_id: base.get("base_scheme_id"),
});
newColorScheme.save().then(() => {
this.model.pushObject(newColorScheme);
newColorScheme.set("savingStatus", null);
this.replaceRoute("adminCustomize.colors.show", newColorScheme);
});
},
newColorScheme() {
showModal("admin-color-scheme-select-base", {
model: this.baseColorSchemes,
admin: true,
});
},
},
});
@@ -0,0 +1,36 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import bootbox from "bootbox";
export default Controller.extend({
@discourseComputed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
},
@discourseComputed("model.changed", "model.isSaving")
saveDisabled(changed, isSaving) {
return !changed || isSaving;
},
actions: {
save() {
if (!this.model.saving) {
this.set("saving", true);
this.model
.update(this.model.getProperties("html", "css"))
.catch((e) => {
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("admin.customize.email_style.save_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg);
})
.finally(() => this.set("model.changed", false));
}
},
},
});
@@ -0,0 +1,62 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { action } from "@ember/object";
import { inject as controller } from "@ember/controller";
import bootbox from "bootbox";
export default Controller.extend(bufferedProperty("emailTemplate"), {
adminCustomizeEmailTemplates: controller(),
emailTemplate: null,
saved: false,
@discourseComputed("buffered.body", "buffered.subject")
saveDisabled(body, subject) {
return (
this.emailTemplate.body === body && this.emailTemplate.subject === subject
);
},
@discourseComputed("buffered")
hasMultipleSubjects(buffered) {
if (buffered.getProperties("subject")["subject"]) {
return false;
} else {
return buffered.getProperties("id")["id"];
}
},
@action
saveChanges() {
this.set("saved", false);
const buffered = this.buffered;
this.emailTemplate
.save(buffered.getProperties("subject", "body"))
.then(() => {
this.set("saved", true);
})
.catch(popupAjaxError);
},
@action
revertChanges() {
this.set("saved", false);
bootbox.confirm(
I18n.t("admin.customize.email_templates.revert_confirm"),
(result) => {
if (result) {
this.emailTemplate
.revert()
.then((props) => {
const buffered = this.buffered;
buffered.setProperties(props);
this.commitBuffer();
})
.catch(popupAjaxError);
}
}
);
},
});
@@ -0,0 +1,18 @@
import { sort } from "@ember/object/computed";
import { action } from "@ember/object";
import Controller from "@ember/controller";
export default Controller.extend({
sortedTemplates: sort("emailTemplates", "titleSorting"),
init() {
this._super(...arguments);
this.set("titleSorting", ["title"]);
},
@action
onSelectTemplate(template) {
this.transitionToRoute("adminCustomizeEmailTemplates.edit", template);
},
});
@@ -0,0 +1,47 @@
import { not } from "@ember/object/computed";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { propertyEqual } from "discourse/lib/computed";
export default Controller.extend(bufferedProperty("model"), {
saved: false,
isSaving: false,
saveDisabled: propertyEqual("model.robots_txt", "buffered.robots_txt"),
resetDisbaled: not("model.overridden"),
actions: {
save() {
this.setProperties({
isSaving: true,
saved: false,
});
ajax("robots.json", {
type: "PUT",
data: { robots_txt: this.buffered.get("robots_txt") },
})
.then((data) => {
this.commitBuffer();
this.set("saved", true);
this.set("model.overridden", data.overridden);
})
.finally(() => this.set("isSaving", false));
},
reset() {
this.setProperties({
isSaving: true,
saved: false,
});
ajax("robots.json", { type: "DELETE" })
.then((data) => {
this.buffered.set("robots_txt", data.robots_txt);
this.commitBuffer();
this.set("saved", true);
this.set("model.overridden", false);
})
.finally(() => this.set("isSaving", false));
},
},
});
@@ -0,0 +1,68 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import { url } from "discourse/lib/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
section: null,
currentTarget: 0,
maximized: false,
previewUrl: url("model.id", "/admin/themes/%@/preview"),
showAdvanced: false,
editRouteName: "adminCustomizeThemes.edit",
showRouteName: "adminCustomizeThemes.show",
setTargetName: function (name) {
const target = this.get("model.targets").find((t) => t.name === name);
this.set("currentTarget", target && target.id);
},
@discourseComputed("currentTarget")
currentTargetName(id) {
const target = this.get("model.targets").find(
(t) => t.id === parseInt(id, 10)
);
return target && target.name;
},
@discourseComputed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
},
@discourseComputed("model.changed", "model.isSaving")
saveDisabled(changed, isSaving) {
return !changed || isSaving;
},
actions: {
save() {
this.set("saving", true);
this.model.saveChanges("theme_fields").finally(() => {
this.set("saving", false);
});
},
fieldAdded(target, name) {
this.replaceRoute(this.editRouteName, this.get("model.id"), target, name);
},
onlyOverriddenChanged(onlyShowOverridden) {
if (onlyShowOverridden) {
if (!this.model.hasEdited(this.currentTargetName, this.fieldName)) {
let firstTarget = this.get("model.targets").find((t) => t.edited);
let firstField = this.get(`model.fields.${firstTarget.name}`).find(
(f) => f.edited
);
this.replaceRoute(
this.editRouteName,
this.get("model.id"),
firstTarget.name,
firstField.name
);
}
}
},
},
});

Some files were not shown because too many files have changed in this diff Show More