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,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
);
}
}
},
},
});
@@ -0,0 +1,389 @@
import I18n from "I18n";
import { makeArray } from "discourse-common/lib/helpers";
import {
empty,
filterBy,
match,
mapBy,
notEmpty,
} from "@ember/object/computed";
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import { url } from "discourse/lib/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import ThemeSettings from "admin/models/theme-settings";
import { THEMES, COMPONENTS } from "admin/models/theme";
import EmberObject from "@ember/object";
import bootbox from "bootbox";
const THEME_UPLOAD_VAR = 2;
export default Controller.extend({
downloadUrl: url("model.id", "/admin/customize/themes/%@/export"),
previewUrl: url("model.id", "/admin/themes/%@/preview"),
addButtonDisabled: empty("selectedChildThemeId"),
editRouteName: "adminCustomizeThemes.edit",
parentThemesNames: mapBy("model.parentThemes", "name"),
availableParentThemes: filterBy("allThemes", "component", false),
availableActiveParentThemes: filterBy("availableParentThemes", "isActive"),
availableThemesNames: mapBy("availableParentThemes", "name"),
availableActiveThemesNames: mapBy("availableActiveParentThemes", "name"),
availableActiveChildThemes: filterBy("availableChildThemes", "hasParents"),
availableComponentsNames: mapBy("availableChildThemes", "name"),
availableActiveComponentsNames: mapBy("availableActiveChildThemes", "name"),
childThemesNames: mapBy("model.childThemes", "name"),
extraFiles: filterBy("model.theme_fields", "target", "extra_js"),
@discourseComputed("model.editedFields")
editedFieldsFormatted() {
const descriptions = [];
["common", "desktop", "mobile"].forEach((target) => {
const fields = this.editedFieldsForTarget(target);
if (fields.length < 1) {
return;
}
let resultString = I18n.t("admin.customize.theme." + target);
const formattedFields = fields
.map((f) => I18n.t("admin.customize.theme." + f.name + ".text"))
.join(" , ");
resultString += `: ${formattedFields}`;
descriptions.push(resultString);
});
return descriptions;
},
@discourseComputed("colorSchemeId", "model.color_scheme_id")
colorSchemeChanged(colorSchemeId, existingId) {
colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId, 10);
return colorSchemeId !== existingId;
},
@discourseComputed("availableChildThemes", "model.childThemes.[]", "model")
selectableChildThemes(available, childThemes) {
if (available) {
const themes = !childThemes
? available
: available.filter((theme) => childThemes.indexOf(theme) === -1);
return themes.length === 0 ? null : themes;
}
},
@discourseComputed("model.parentThemes.[]")
relativesSelectorSettingsForComponent() {
return EmberObject.create({
list_type: "compact",
type: "list",
preview: null,
anyValue: false,
setting: "parent_theme_ids",
label: I18n.t("admin.customize.theme.component_on_themes"),
choices: this.availableThemesNames,
default: this.parentThemesNames.join("|"),
value: this.parentThemesNames.join("|"),
defaultValues: this.availableActiveThemesNames.join("|"),
allThemes: this.allThemes,
setDefaultValuesLabel: I18n.t("admin.customize.theme.add_all_themes"),
});
},
@discourseComputed("model.parentThemes.[]")
relativesSelectorSettingsForTheme() {
return EmberObject.create({
list_type: "compact",
type: "list",
preview: null,
anyValue: false,
setting: "child_theme_ids",
label: I18n.t("admin.customize.theme.included_components"),
choices: this.availableComponentsNames,
default: this.childThemesNames.join("|"),
value: this.childThemesNames.join("|"),
defaultValues: this.availableActiveComponentsNames.join("|"),
allThemes: this.allThemes,
setDefaultValuesLabel: I18n.t("admin.customize.theme.add_all"),
});
},
@discourseComputed("allThemes", "model.component", "model")
availableChildThemes(allThemes) {
if (!this.get("model.component")) {
const themeId = this.get("model.id");
return allThemes.filter(
(theme) => theme.get("id") !== themeId && theme.get("component")
);
}
},
@discourseComputed("model.component")
convertKey(component) {
const type = component ? "component" : "theme";
return `admin.customize.theme.convert_${type}`;
},
@discourseComputed("model.component")
convertIcon(component) {
return component ? "cube" : "";
},
@discourseComputed("model.component")
convertTooltip(component) {
const type = component ? "component" : "theme";
return `admin.customize.theme.convert_${type}_tooltip`;
},
@discourseComputed("model.settings")
settings(settings) {
return settings.map((setting) => ThemeSettings.create(setting));
},
hasSettings: notEmpty("settings"),
@discourseComputed("model.translations")
translations(translations) {
return translations.map((setting) => ThemeSettings.create(setting));
},
hasTranslations: notEmpty("translations"),
@discourseComputed("model.remoteError", "updatingRemote")
showRemoteError(errorMessage, updating) {
return errorMessage && !updating;
},
editedFieldsForTarget(target) {
return this.get("model.editedFields").filter(
(field) => field.target === target
);
},
commitSwitchType() {
const model = this.model;
const newValue = !model.get("component");
model.set("component", newValue);
if (newValue) {
this.set("parentController.currentTab", COMPONENTS);
} else {
this.set("parentController.currentTab", THEMES);
}
model
.saveChanges("component")
.then(() => {
this.set("colorSchemeId", null);
model.setProperties({
default: false,
color_scheme_id: null,
user_selectable: false,
child_themes: [],
childThemes: [],
});
this.get("parentController.model.content").forEach((theme) => {
const children = makeArray(theme.get("childThemes"));
const rawChildren = makeArray(theme.get("child_themes"));
const index = children ? children.indexOf(model) : -1;
if (index > -1) {
children.splice(index, 1);
rawChildren.splice(index, 1);
theme.setProperties({
childThemes: children,
child_themes: rawChildren,
});
}
});
})
.catch(popupAjaxError);
},
transitionToEditRoute() {
this.transitionToRoute(
this.editRouteName,
this.get("model.id"),
"common",
"scss"
);
},
sourceIsHttp: match("model.remote_theme.remote_url", /^http(s)?:\/\//),
@discourseComputed(
"model.remote_theme.remote_url",
"model.remote_theme.branch"
)
remoteThemeLink(remoteThemeUrl, remoteThemeBranch) {
return remoteThemeBranch
? `${remoteThemeUrl.replace(/\.git$/, "")}/tree/${remoteThemeBranch}`
: remoteThemeUrl;
},
actions: {
updateToLatest() {
this.set("updatingRemote", true);
this.model
.updateToLatest()
.catch(popupAjaxError)
.finally(() => {
this.set("updatingRemote", false);
});
},
checkForThemeUpdates() {
this.set("updatingRemote", true);
this.model
.checkForUpdates()
.catch(popupAjaxError)
.finally(() => {
this.set("updatingRemote", false);
});
},
addUploadModal() {
showModal("admin-add-upload", { admin: true, name: "" });
},
addUpload(info) {
let model = this.model;
model.setField("common", info.name, "", info.upload_id, THEME_UPLOAD_VAR);
model.saveChanges("theme_fields").catch((e) => popupAjaxError(e));
},
cancelChangeScheme() {
this.set("colorSchemeId", this.get("model.color_scheme_id"));
},
changeScheme() {
let schemeId = this.colorSchemeId;
this.set(
"model.color_scheme_id",
schemeId === null ? null : parseInt(schemeId, 10)
);
this.model.saveChanges("color_scheme_id");
},
startEditingName() {
this.set("oldName", this.get("model.name"));
this.set("editingName", true);
},
cancelEditingName() {
this.set("model.name", this.oldName);
this.set("editingName", false);
},
finishedEditingName() {
this.model.saveChanges("name");
this.set("editingName", false);
},
editTheme() {
if (this.get("model.remote_theme.is_git")) {
bootbox.confirm(
I18n.t("admin.customize.theme.edit_confirm"),
(result) => {
if (result) {
this.transitionToEditRoute();
}
}
);
} else {
this.transitionToEditRoute();
}
},
applyDefault() {
const model = this.model;
model.saveChanges("default").then(() => {
if (model.get("default")) {
this.allThemes.forEach((theme) => {
if (theme !== model && theme.get("default")) {
theme.set("default", false);
}
});
}
});
},
applyUserSelectable() {
this.model.saveChanges("user_selectable");
},
addChildTheme() {
let themeId = parseInt(this.selectedChildThemeId, 10);
let theme = this.allThemes.findBy("id", themeId);
this.model.addChildTheme(theme).then(() => this.store.findAll("theme"));
},
removeUpload(upload) {
return bootbox.confirm(
I18n.t("admin.customize.theme.delete_upload_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.model.removeField(upload);
}
}
);
},
removeChildTheme(theme) {
this.model
.removeChildTheme(theme)
.then(() => this.store.findAll("theme"));
},
destroy() {
return bootbox.confirm(
I18n.t("admin.customize.delete_confirm", {
theme_name: this.get("model.name"),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
const model = this.model;
model.setProperties({ recentlyInstalled: false });
model.destroyRecord().then(() => {
this.allThemes.removeObject(model);
this.transitionToRoute("adminCustomizeThemes");
});
}
}
);
},
switchType() {
const relatives = this.get("model.component")
? this.parentThemes
: this.get("model.childThemes");
if (relatives && relatives.length > 0) {
const names = relatives.map((relative) => relative.get("name"));
bootbox.confirm(
I18n.t(`${this.convertKey}_alert`, {
relatives: names.join(", "),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.commitSwitchType();
}
}
);
} else {
this.commitSwitchType();
}
},
enableComponent() {
this.model.set("enabled", true);
this.model
.saveChanges("enabled")
.catch(() => this.model.set("enabled", false));
},
disableComponent() {
this.model.set("enabled", false);
this.model
.saveChanges("enabled")
.catch(() => this.model.set("enabled", true));
},
},
});
@@ -0,0 +1,22 @@
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import { THEMES } from "admin/models/theme";
export default Controller.extend({
currentTab: THEMES,
@discourseComputed("model", "model.@each.component")
fullThemes(themes) {
return themes.filter((t) => !t.get("component"));
},
@discourseComputed("model", "model.@each.component")
childThemes(themes) {
return themes.filter((t) => t.get("component"));
},
@discourseComputed("model", "model.@each.component")
installedThemes(themes) {
return themes.map((t) => t.name);
},
});
@@ -0,0 +1,151 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { inject } from "@ember/controller";
import Controller from "@ember/controller";
import { setting } from "discourse/lib/computed";
import AdminDashboard from "admin/models/admin-dashboard";
import Report from "admin/models/report";
import PeriodComputationMixin from "admin/mixins/period-computation";
import { computed } from "@ember/object";
import getURL from "discourse-common/lib/get-url";
function staticReport(reportType) {
return computed("reports.[]", function () {
return makeArray(this.reports).find((report) => report.type === reportType);
});
}
export default Controller.extend(PeriodComputationMixin, {
isLoading: false,
dashboardFetchedAt: null,
exceptionController: inject("exception"),
logSearchQueriesEnabled: setting("log_search_queries"),
@discourseComputed("siteSettings.dashboard_general_tab_activity_metrics")
activityMetrics(metrics) {
return (metrics || "").split("|").filter(Boolean);
},
hiddenReports: computed("siteSettings.dashboard_hidden_reports", function () {
return (this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean);
}),
isActivityMetricsVisible: computed(
"activityMetrics",
"hiddenReports",
function () {
return (
this.activityMetrics.length &&
this.activityMetrics.some((x) => !this.hiddenReports.includes(x))
);
}
),
isSearchReportsVisible: computed("hiddenReports", function () {
return ["top_referred_topics", "trending_search"].some(
(x) => !this.hiddenReports.includes(x)
);
}),
isCommunityHealthVisible: computed("hiddenReports", function () {
return [
"consolidated_page_views",
"signups",
"topics",
"posts",
"dau_by_mau",
"daily_engaged_users",
"new_contributors",
].some((x) => !this.hiddenReports.includes(x));
}),
@discourseComputed
activityMetricsFilters() {
return {
startDate: this.lastMonth,
endDate: this.today,
};
},
@discourseComputed
topReferredTopicsOptions() {
return {
table: { total: false, limit: 8 },
};
},
@discourseComputed
topReferredTopicsFilters() {
return {
startDate: moment().subtract(6, "days").startOf("day"),
endDate: this.today,
};
},
@discourseComputed
trendingSearchFilters() {
return {
startDate: moment().subtract(1, "month").startOf("day"),
endDate: this.today,
};
},
@discourseComputed
trendingSearchOptions() {
return {
table: { total: false, limit: 8 },
};
},
@discourseComputed
trendingSearchDisabledLabel() {
return I18n.t("admin.dashboard.reports.trending_search.disabled", {
basePath: getURL(""),
});
},
usersByTypeReport: staticReport("users_by_type"),
usersByTrustLevelReport: staticReport("users_by_trust_level"),
storageReport: staticReport("storage_report"),
fetchDashboard() {
if (this.isLoading) {
return;
}
if (
!this.dashboardFetchedAt ||
moment().subtract(30, "minutes").toDate() > this.dashboardFetchedAt
) {
this.set("isLoading", true);
AdminDashboard.fetchGeneral()
.then((adminDashboardModel) => {
this.setProperties({
dashboardFetchedAt: new Date(),
model: adminDashboardModel,
reports: makeArray(adminDashboardModel.reports).map((x) =>
Report.create(x)
),
});
})
.catch((e) => {
this.exceptionController.set("thrown", e.jqXHR);
this.replaceRoute("exception");
})
.finally(() => this.set("isLoading", false));
}
},
@discourseComputed("startDate", "endDate")
filters(startDate, endDate) {
return { startDate, endDate };
},
_reportsForPeriodURL(period) {
return getURL(`/admin?period=${period}`);
},
});
@@ -0,0 +1,51 @@
import getURL from "discourse-common/lib/get-url";
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import PeriodComputationMixin from "admin/mixins/period-computation";
import { computed } from "@ember/object";
export default Controller.extend(PeriodComputationMixin, {
@discourseComputed
flagsStatusOptions() {
return {
table: {
total: false,
perPage: 10,
},
};
},
isModeratorsActivityVisible: computed(
"siteSettings.dashboard_hidden_reports",
function () {
return !(this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean)
.includes("moderators_activity");
}
),
@discourseComputed
userFlaggingRatioOptions() {
return {
table: {
total: false,
perPage: 10,
},
};
},
@discourseComputed("startDate", "endDate")
filters(startDate, endDate) {
return { startDate, endDate };
},
@discourseComputed("lastWeek", "endDate")
lastWeekfilters(startDate, endDate) {
return { startDate, endDate };
},
_reportsForPeriodURL(period) {
return getURL(`/admin/dashboard/moderation?period=${period}`);
},
});
@@ -0,0 +1,44 @@
import discourseComputed from "discourse-common/utils/decorators";
import { debounce } from "@ember/runloop";
import Controller from "@ember/controller";
import { INPUT_DELAY } from "discourse-common/config/environment";
const { get } = Ember;
export default Controller.extend({
filter: null,
@discourseComputed(
"model.[]",
"filter",
"siteSettings.dashboard_hidden_reports"
)
filterReports(reports, filter) {
if (filter) {
filter = filter.toLowerCase();
reports = reports.filter((report) => {
return (
(get(report, "title") || "").toLowerCase().indexOf(filter) > -1 ||
(get(report, "description") || "").toLowerCase().indexOf(filter) > -1
);
});
}
const hiddenReports = (this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean);
reports = reports.filter((report) => !hiddenReports.includes(report.type));
return reports;
},
actions: {
filterReports(filter) {
debounce(this, this._performFiltering, filter, INPUT_DELAY);
},
},
_performFiltering(filter) {
this.set("filter", filter);
},
});
@@ -0,0 +1,109 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller, { inject } from "@ember/controller";
import { setting } from "discourse/lib/computed";
import { computed } from "@ember/object";
import AdminDashboard from "admin/models/admin-dashboard";
import VersionCheck from "admin/models/version-check";
const PROBLEMS_CHECK_MINUTES = 1;
export default Controller.extend({
isLoading: false,
dashboardFetchedAt: null,
exceptionController: inject("exception"),
showVersionChecks: setting("version_checks"),
@discourseComputed("problems.length")
foundProblems(problemsLength) {
return this.currentUser.get("admin") && (problemsLength || 0) > 0;
},
visibleTabs: computed("siteSettings.dashboard_visible_tabs", function () {
return (this.siteSettings.dashboard_visible_tabs || "")
.split("|")
.filter(Boolean);
}),
isModerationTabVisible: computed("visibleTabs", function () {
return this.visibleTabs.includes("moderation");
}),
isSecurityTabVisible: computed("visibleTabs", function () {
return this.visibleTabs.includes("security");
}),
isReportsTabVisible: computed("visibleTabs", function () {
return this.visibleTabs.includes("reports");
}),
fetchProblems() {
if (this.isLoadingProblems) {
return;
}
if (
!this.problemsFetchedAt ||
moment().subtract(PROBLEMS_CHECK_MINUTES, "minutes").toDate() >
this.problemsFetchedAt
) {
this._loadProblems();
}
},
fetchDashboard() {
const versionChecks = this.siteSettings.version_checks;
if (this.isLoading || !versionChecks) {
return;
}
if (
!this.dashboardFetchedAt ||
moment().subtract(30, "minutes").toDate() > this.dashboardFetchedAt
) {
this.set("isLoading", true);
AdminDashboard.fetch()
.then((model) => {
let properties = {
dashboardFetchedAt: new Date(),
};
if (versionChecks) {
properties.versionCheck = VersionCheck.create(model.version_check);
}
this.setProperties(properties);
})
.catch((e) => {
this.exceptionController.set("thrown", e.jqXHR);
this.replaceRoute("exception");
})
.finally(() => {
this.set("isLoading", false);
});
}
},
_loadProblems() {
this.setProperties({
loadingProblems: true,
problemsFetchedAt: new Date(),
});
AdminDashboard.fetchProblems()
.then((model) => this.set("problems", model.problems))
.finally(() => this.set("loadingProblems", false));
},
@discourseComputed("problemsFetchedAt")
problemsTimestamp(problemsFetchedAt) {
return moment(problemsFetchedAt).locale("en").format("LLL");
},
actions: {
refreshProblems() {
this._loadProblems();
},
},
});
@@ -0,0 +1,31 @@
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
email: null,
text: null,
elided: null,
format: null,
loading: null,
actions: {
run() {
this.set("loading", true);
ajax("/admin/email/advanced-test", {
type: "POST",
data: { email: this.email },
})
.then((data) => {
this.setProperties({
text: data.text,
elided: data.elided,
format: data.format,
});
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
},
});
@@ -0,0 +1,11 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import discourseDebounce from "discourse/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
export default AdminEmailLogsController.extend({
@observes("filter.{status,user,address,type}")
filterEmailLogs: discourseDebounce(function () {
this.loadLogs();
}, INPUT_DELAY),
});
@@ -0,0 +1,59 @@
import I18n from "I18n";
import { empty } from "@ember/object/computed";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { observes } from "discourse-common/utils/decorators";
import bootbox from "bootbox";
export default Controller.extend({
/**
Is the "send test email" button disabled?
@property sendTestEmailDisabled
**/
sendTestEmailDisabled: empty("testEmailAddress"),
/**
Clears the 'sentTestEmail' property on successful send.
@method testEmailAddressChanged
**/
@observes("testEmailAddress")
testEmailAddressChanged: function () {
this.set("sentTestEmail", false);
},
actions: {
/**
Sends a test email to the currently entered email address
@method sendTestEmail
**/
sendTestEmail: function () {
this.setProperties({
sendingEmail: true,
sentTestEmail: false,
});
ajax("/admin/email/test", {
type: "POST",
data: { email_address: this.testEmailAddress },
})
.then((response) =>
this.set("sentTestEmailMessage", response.sent_test_email_message)
)
.catch((e) => {
if (e.responseJSON && e.responseJSON.errors) {
bootbox.alert(
I18n.t("admin.email.error", {
server_error: e.responseJSON.errors[0],
})
);
} else {
bootbox.alert(I18n.t("admin.email.test_error"));
}
})
.finally(() => this.set("sendingEmail", false));
},
},
});
@@ -0,0 +1,37 @@
import Controller from "@ember/controller";
import EmailLog from "admin/models/email-log";
export default Controller.extend({
loading: false,
loadLogs(sourceModel, loadMore) {
if ((loadMore && this.loading) || this.get("model.allLoaded")) {
return;
}
this.set("loading", true);
sourceModel = sourceModel || EmailLog;
return sourceModel
.findAll(this.filter, loadMore ? this.get("model.length") : null)
.then((logs) => {
if (this.model && loadMore && logs.length < 50) {
this.model.set("allLoaded", true);
}
if (this.model && loadMore) {
this.model.addObjects(logs);
} else {
this.set("model", logs);
}
})
.finally(() => this.set("loading", false));
},
actions: {
loadMore() {
this.loadLogs(EmailLog, true);
},
},
});
@@ -0,0 +1,59 @@
import { empty, or, notEmpty } from "@ember/object/computed";
import Controller from "@ember/controller";
import EmailPreview from "admin/models/email-preview";
import { popupAjaxError } from "discourse/lib/ajax-error";
import bootbox from "bootbox";
export default Controller.extend({
username: null,
lastSeen: null,
emailEmpty: empty("email"),
sendEmailDisabled: or("emailEmpty", "sendingEmail"),
showSendEmailForm: notEmpty("model.html_content"),
htmlEmpty: empty("model.html_content"),
actions: {
refresh() {
const model = this.model;
this.set("loading", true);
this.set("sentEmail", false);
let username = this.username;
if (!username) {
username = this.currentUser.get("username");
this.set("username", username);
}
EmailPreview.findDigest(username, this.lastSeen).then((email) => {
model.setProperties(
email.getProperties("html_content", "text_content")
);
this.set("loading", false);
});
},
toggleShowHtml() {
this.toggleProperty("showHtml");
},
sendEmail() {
this.set("sendingEmail", true);
this.set("sentEmail", false);
EmailPreview.sendDigest(this.username, this.lastSeen, this.email)
.then((result) => {
if (result.errors) {
bootbox.alert(result.errors);
} else {
this.set("sentEmail", true);
}
})
.catch(popupAjaxError)
.finally(() => {
this.set("sendingEmail", false);
});
},
},
});
@@ -0,0 +1,18 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import discourseDebounce from "discourse/lib/debounce";
import IncomingEmail from "admin/models/incoming-email";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
export default AdminEmailLogsController.extend({
@observes("filter.{status,from,to,subject}")
filterIncomingEmails: discourseDebounce(function () {
this.loadLogs(IncomingEmail);
}, INPUT_DELAY),
actions: {
loadMore() {
this.loadLogs(IncomingEmail, true);
},
},
});
@@ -0,0 +1,18 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import discourseDebounce from "discourse/lib/debounce";
import IncomingEmail from "admin/models/incoming-email";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
export default AdminEmailLogsController.extend({
@observes("filter.{status,from,to,subject,error}")
filterIncomingEmails: discourseDebounce(function () {
this.loadLogs(IncomingEmail);
}, INPUT_DELAY),
actions: {
loadMore() {
this.loadLogs(IncomingEmail, true);
},
},
});
@@ -0,0 +1,11 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import discourseDebounce from "discourse/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
export default AdminEmailLogsController.extend({
@observes("filter.{status,user,address,type,reply_key}")
filterEmailLogs: discourseDebounce(function () {
this.loadLogs();
}, INPUT_DELAY),
});
@@ -0,0 +1,11 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import discourseDebounce from "discourse/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
export default AdminEmailLogsController.extend({
@observes("filter.{status,user,address,type}")
filterEmailLogs: discourseDebounce(function () {
this.loadLogs();
}, INPUT_DELAY),
});
@@ -0,0 +1,55 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
saved: false,
embedding: null,
// show settings if we have at least one created host
@discourseComputed("embedding.embeddable_hosts.@each.isCreated")
showSecondary() {
const hosts = this.get("embedding.embeddable_hosts");
return hosts.length && hosts.findBy("isCreated");
},
@discourseComputed("embedding.base_url")
embeddingCode(baseUrl) {
const html = `<div id='discourse-comments'></div>
<script type="text/javascript">
DiscourseEmbed = { discourseUrl: '${baseUrl}/',
discourseEmbedUrl: 'REPLACE_ME' };
(function() {
var d = document.createElement('script'); d.type = 'text/javascript'; d.async = true;
d.src = DiscourseEmbed.discourseUrl + 'javascripts/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(d);
})();
</script>`;
return html;
},
actions: {
saveChanges() {
const embedding = this.embedding;
const updates = embedding.getProperties(embedding.get("fields"));
this.set("saved", false);
this.embedding
.update(updates)
.then(() => this.set("saved", true))
.catch(popupAjaxError);
},
addHost() {
const host = this.store.createRecord("embeddable-host");
this.get("embedding.embeddable_hosts").pushObject(host);
},
deleteHost(host) {
this.get("embedding.embeddable_hosts").removeObject(host);
},
},
});
@@ -0,0 +1,76 @@
import I18n from "I18n";
import { sort } from "@ember/object/computed";
import EmberObject, { action, computed } from "@ember/object";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
const ALL_FILTER = "all";
export default Controller.extend({
filter: null,
sorting: null,
init() {
this._super(...arguments);
this.setProperties({
filter: ALL_FILTER,
sorting: ["group", "name"],
});
},
sortedEmojis: sort("filteredEmojis.[]", "sorting"),
emojiGroups: computed("model", {
get() {
return this.model.mapBy("group").uniq();
},
}),
sortingGroups: computed("emojiGroups.[]", {
get() {
return [ALL_FILTER].concat(this.emojiGroups);
},
}),
filteredEmojis: computed("model.[]", "filter", {
get() {
if (!this.filter || this.filter === ALL_FILTER) {
return this.model;
} else {
return this.model.filterBy("group", this.filter);
}
},
}),
@action
filterGroups(value) {
this.set("filter", value);
},
@action
emojiUploaded(emoji, group) {
emoji.url += "?t=" + new Date().getTime();
emoji.group = group;
this.model.pushObject(EmberObject.create(emoji));
},
@action
destroyEmoji(emoji) {
return bootbox.confirm(
I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }),
I18n.t("no_value"),
I18n.t("yes_value"),
(destroy) => {
if (destroy) {
return ajax("/admin/customize/emojis/" + emoji.get("name"), {
type: "DELETE",
}).then(() => {
this.model.removeObject(emoji);
});
}
}
);
},
});
@@ -0,0 +1,29 @@
import Controller from "@ember/controller";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import ScreenedEmail from "admin/models/screened-email";
export default Controller.extend({
loading: false,
actions: {
clearBlock(row) {
row.clearBlock().then(function () {
// feeling lazy
window.location.reload();
});
},
exportScreenedEmailList() {
exportEntity("screened_email").then(outputExportResult);
},
},
show() {
this.set("loading", true);
ScreenedEmail.findAll().then((result) => {
this.set("model", result);
this.set("loading", false);
});
},
});
@@ -0,0 +1,140 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import discourseDebounce from "discourse/lib/debounce";
import { outputExportResult } from "discourse/lib/export-result";
import { exportEntity } from "discourse/lib/export-csv";
import ScreenedIpAddress from "admin/models/screened-ip-address";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
import bootbox from "bootbox";
export default Controller.extend({
loading: false,
filter: null,
savedIpAddress: null,
@observes("filter")
show: discourseDebounce(function () {
this.set("loading", true);
ScreenedIpAddress.findAll(this.filter).then((result) => {
this.setProperties({ model: result, loading: false });
});
}, INPUT_DELAY),
actions: {
allow(record) {
record.set("action_name", "do_nothing");
record.save();
},
block(record) {
record.set("action_name", "block");
record.save();
},
edit(record) {
if (!record.get("editing")) {
this.set("savedIpAddress", record.get("ip_address"));
}
record.set("editing", true);
},
cancel(record) {
const savedIpAddress = this.savedIpAddress;
if (savedIpAddress && record.get("editing")) {
record.set("ip_address", savedIpAddress);
}
record.set("editing", false);
},
save(record) {
const wasEditing = record.get("editing");
record.set("editing", false);
record
.save()
.then(() => this.set("savedIpAddress", null))
.catch((e) => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
bootbox.alert(
I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
);
} else {
bootbox.alert(I18n.t("generic_error"));
}
if (wasEditing) {
record.set("editing", true);
}
});
},
destroy(record) {
return bootbox.confirm(
I18n.t("admin.logs.screened_ips.delete_confirm", {
ip_address: record.get("ip_address"),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
record
.destroy()
.then((deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
bootbox.alert(I18n.t("generic_error"));
}
})
.catch((e) => {
bootbox.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`,
})
);
});
}
}
);
},
recordAdded(arg) {
this.model.unshiftObject(arg);
},
rollUp() {
return bootbox.confirm(
I18n.t("admin.logs.screened_ips.roll_up_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
this.set("loading", true);
return ScreenedIpAddress.rollUp().then((results) => {
if (results && results.subnets) {
if (results.subnets.length > 0) {
this.send("show");
bootbox.alert(
I18n.t("admin.logs.screened_ips.rolled_up_some_subnets", {
subnets: results.subnets.join(", "),
})
);
} else {
this.set("loading", false);
bootbox.alert(
I18n.t("admin.logs.screened_ips.rolled_up_no_subnet")
);
}
}
});
}
}
);
},
exportScreenedIpList() {
exportEntity("screened_ip").then(outputExportResult);
},
},
});
@@ -0,0 +1,22 @@
import Controller from "@ember/controller";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import ScreenedUrl from "admin/models/screened-url";
export default Controller.extend({
loading: false,
show() {
this.set("loading", true);
ScreenedUrl.findAll().then((result) => {
this.set("model", result);
this.set("loading", false);
});
},
actions: {
exportScreenedUrlList() {
exportEntity("screened_url").then(outputExportResult);
},
},
});
@@ -0,0 +1,135 @@
import Controller from "@ember/controller";
import EmberObject from "@ember/object";
import { scheduleOnce } from "@ember/runloop";
import discourseComputed from "discourse-common/utils/decorators";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import I18n from "I18n";
export default Controller.extend({
queryParams: ["filters"],
model: null,
filters: null,
userHistoryActions: null,
@discourseComputed("filters.action_name")
actionFilter(name) {
return name ? I18n.t("admin.logs.staff_actions.actions." + name) : null;
},
@discourseComputed("filters")
filtersExists(filters) {
return filters && Object.keys(filters).length > 0;
},
_refresh() {
this.store.findAll("staff-action-log", this.filters).then((result) => {
this.set("model", result);
if (!this.userHistoryActions) {
this.set(
"userHistoryActions",
result.extras.user_history_actions
.map((action) => ({
id: action.id,
action_id: action.action_id,
name: I18n.t("admin.logs.staff_actions.actions." + action.id),
name_raw: action.id,
}))
.sort((a, b) => a.name.localeCompare(b.name))
);
}
});
},
scheduleRefresh() {
scheduleOnce("afterRender", this, this._refresh);
},
resetFilters() {
this.setProperties({
model: EmberObject.create({ loadingMore: true }),
filters: EmberObject.create(),
});
this.scheduleRefresh();
},
changeFilters(props) {
this.set("model", EmberObject.create({ loadingMore: true }));
if (!this.filters) {
this.set("filters", EmberObject.create());
}
Object.keys(props).forEach((key) => {
if (props[key] === undefined || props[key] === null) {
this.filters.set(key, undefined);
delete this.filters[key];
} else {
this.filters.set(key, props[key]);
}
});
this.send("onFiltersChange", this.filters);
this.scheduleRefresh();
},
actions: {
filterActionIdChanged(filterActionId) {
if (filterActionId) {
this.changeFilters({
action_name: filterActionId,
action_id: this.userHistoryActions.findBy("id", filterActionId)
.action_id,
});
}
},
clearFilter(key) {
if (key === "actionFilter") {
this.set("filterActionId", null);
this.changeFilters({
action_name: null,
action_id: null,
custom_type: null,
});
} else {
this.changeFilters({ [key]: null });
}
},
clearAllFilters() {
this.set("filterActionId", null);
this.resetFilters();
},
filterByAction(logItem) {
this.changeFilters({
action_name: logItem.get("action_name"),
action_id: logItem.get("action"),
custom_type: logItem.get("custom_type"),
});
},
filterByStaffUser(acting_user) {
this.changeFilters({ acting_user: acting_user.username });
},
filterByTargetUser(target_user) {
this.changeFilters({ target_user: target_user.username });
},
filterBySubject(subject) {
this.changeFilters({ subject: subject });
},
exportStaffActionLogs() {
exportEntity("staff_action").then(outputExportResult);
},
loadMore() {
this.model.loadMore();
},
},
});
@@ -0,0 +1,60 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import discourseDebounce from "discourse/lib/debounce";
import Permalink from "admin/models/permalink";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
import bootbox from "bootbox";
export default Controller.extend({
loading: false,
filter: null,
@observes("filter")
show: discourseDebounce(function () {
Permalink.findAll(this.filter).then((result) => {
this.set("model", result);
this.set("loading", false);
});
}, INPUT_DELAY),
actions: {
recordAdded(arg) {
this.model.unshiftObject(arg);
},
copyUrl(pl) {
let linkElement = document.querySelector(`#admin-permalink-${pl.id}`);
let textArea = document.createElement("textarea");
textArea.value = linkElement.textContent;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("Copy");
textArea.remove();
},
destroy: function (record) {
return bootbox.confirm(
I18n.t("admin.permalink.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
record.destroy().then(
(deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
bootbox.alert(I18n.t("generic_error"));
}
},
function () {
bootbox.alert(I18n.t("generic_error"));
}
);
}
}
);
},
},
});
@@ -0,0 +1,25 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
export default Controller.extend({
@discourseComputed
adminRoutes: function () {
return this.model
.map((p) => {
if (p.get("enabled")) {
return p.admin_route;
}
})
.compact();
},
actions: {
clearFilter() {
this.setProperties({ filter: "", onlyOverridden: false });
},
toggleMenu() {
$(".admin-detail").toggleClass("mobile-closed mobile-open");
},
},
});
@@ -0,0 +1,23 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
export default Controller.extend({
queryParams: ["start_date", "end_date", "filters", "chart_grouping"],
start_date: null,
end_date: null,
filters: null,
chart_grouping: null,
@discourseComputed("model.type")
reportOptions(type) {
let options = { table: { perPage: 50, limit: 50, formatNumbers: false } };
if (type === "top_referred_topics") {
options.table.limit = 10;
}
options.chartGrouping = this.chart_grouping;
return options;
},
});
@@ -0,0 +1,25 @@
import I18n from "I18n";
import Controller from "@ember/controller";
export const DEFAULT_PERIOD = "yearly";
export default Controller.extend({
loading: false,
period: DEFAULT_PERIOD,
searchType: "all",
init() {
this._super(...arguments);
this.searchTypeOptions = [
{
id: "all",
name: I18n.t("admin.logs.search_logs.types.all_search_types"),
},
{ id: "header", name: I18n.t("admin.logs.search_logs.types.header") },
{
id: "full_page",
name: I18n.t("admin.logs.search_logs.types.full_page"),
},
];
},
});
@@ -0,0 +1,30 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import { DEFAULT_PERIOD } from "admin/controllers/admin-search-logs-index";
export default Controller.extend({
loading: false,
term: null,
period: DEFAULT_PERIOD,
searchType: "all",
init() {
this._super(...arguments);
this.searchTypeOptions = [
{
id: "all",
name: I18n.t("admin.logs.search_logs.types.all_search_types"),
},
{ id: "header", name: I18n.t("admin.logs.search_logs.types.header") },
{
id: "full_page",
name: I18n.t("admin.logs.search_logs.types.full_page"),
},
{
id: "click_through_only",
name: I18n.t("admin.logs.search_logs.types.click_through_only"),
},
];
},
});
@@ -0,0 +1,17 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller, { inject as controller } from "@ember/controller";
export default Controller.extend({
adminSiteSettings: controller(),
categoryNameKey: null,
@discourseComputed("adminSiteSettings.visibleSiteSettings", "categoryNameKey")
category(categories, nameKey) {
return (categories || []).findBy("nameKey", nameKey);
},
@discourseComputed("category")
filteredContent(category) {
return category ? category.siteSettings : [];
},
});
@@ -0,0 +1,132 @@
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { alias } from "@ember/object/computed";
import Controller from "@ember/controller";
import discourseDebounce from "discourse/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
export default Controller.extend({
filter: null,
allSiteSettings: alias("model"),
visibleSiteSettings: null,
onlyOverridden: false,
filterContentNow(category) {
// If we have no content, don't bother filtering anything
if (!!isEmpty(this.allSiteSettings)) {
return;
}
let filter, pluginFilter;
if (this.filter) {
filter = this.filter
.toLowerCase()
.split(" ")
.filter((word) => {
if (word.length === 0) {
return false;
}
if (word.startsWith("plugin:")) {
pluginFilter = word.substr("plugin:".length).trim();
return false;
}
return true;
})
.join(" ")
.trim();
}
if (
(!filter || 0 === filter.length) &&
(!pluginFilter || 0 === pluginFilter.length) &&
!this.onlyOverridden
) {
this.set("visibleSiteSettings", this.allSiteSettings);
if (this.categoryNameKey === "all_results") {
this.transitionToRoute("adminSiteSettings");
}
return;
}
const all = {
nameKey: "all_results",
name: I18n.t("admin.site_settings.categories.all_results"),
siteSettings: [],
};
const matchesGroupedByCategory = [all];
const matches = [];
this.allSiteSettings.forEach((settingsCategory) => {
const siteSettings = settingsCategory.siteSettings.filter((item) => {
if (this.onlyOverridden && !item.get("overridden")) {
return false;
}
if (pluginFilter && item.plugin !== pluginFilter) {
return false;
}
if (filter) {
const setting = item.get("setting").toLowerCase();
return (
setting.includes(filter) ||
setting.replace(/_/g, " ").includes(filter) ||
item.get("description").toLowerCase().includes(filter) ||
(item.get("value") || "").toLowerCase().includes(filter)
);
} else {
return true;
}
});
if (siteSettings.length > 0) {
matches.pushObjects(siteSettings);
matchesGroupedByCategory.pushObject({
nameKey: settingsCategory.nameKey,
name: I18n.t(
"admin.site_settings.categories." + settingsCategory.nameKey
),
siteSettings,
count: siteSettings.length,
});
}
});
all.siteSettings.pushObjects(matches.slice(0, 30));
all.hasMore = matches.length > 30;
all.count = all.hasMore ? "30+" : matches.length;
const categoryMatches = matchesGroupedByCategory.findBy(
"nameKey",
category
);
if (!categoryMatches || categoryMatches.count === 0) {
category = "all_results";
}
this.set("visibleSiteSettings", matchesGroupedByCategory);
this.transitionToRoute(
"adminSiteSettingsCategory",
category || "all_results"
);
},
@observes("filter", "onlyOverridden", "model")
filterContent: discourseDebounce(function () {
if (this._skipBounce) {
this.set("_skipBounce", false);
} else {
this.filterContentNow(this.categoryNameKey);
}
}, INPUT_DELAY),
actions: {
clearFilter() {
this.setProperties({ filter: "", onlyOverridden: false });
},
toggleMenu() {
$(".admin-detail").toggleClass("mobile-closed mobile-open");
},
},
});
@@ -0,0 +1,44 @@
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 bootbox from "bootbox";
export default Controller.extend(bufferedProperty("siteText"), {
saved: false,
@discourseComputed("buffered.value")
saveDisabled(value) {
return this.siteText.value === value;
},
actions: {
saveChanges() {
const buffered = this.buffered;
this.siteText
.save(buffered.getProperties("value"))
.then(() => {
this.commitBuffer();
this.set("saved", true);
})
.catch(popupAjaxError);
},
revertChanges() {
this.set("saved", false);
bootbox.confirm(I18n.t("admin.site_text.revert_confirm"), (result) => {
if (result) {
this.siteText
.revert()
.then((props) => {
const buffered = this.buffered;
buffered.setProperties(props);
this.commitBuffer();
})
.catch(popupAjaxError);
}
});
},
},
});
@@ -0,0 +1,43 @@
import { debounce } from "@ember/runloop";
import Controller from "@ember/controller";
let lastSearch;
export default Controller.extend({
searching: false,
siteTexts: null,
preferred: false,
queryParams: ["q", "overridden"],
q: null,
overridden: false,
_performSearch() {
this.store
.find("site-text", this.getProperties("q", "overridden"))
.then((results) => {
this.set("siteTexts", results);
})
.finally(() => this.set("searching", false));
},
actions: {
edit(siteText) {
this.transitionToRoute("adminSiteText.edit", siteText.get("id"));
},
toggleOverridden() {
this.toggleProperty("overridden");
this.set("searching", true);
debounce(this, this._performSearch, 400);
},
search() {
const q = this.q;
if (q !== lastSearch) {
this.set("searching", true);
debounce(this, this._performSearch, 400);
lastSearch = q;
}
},
},
});
@@ -0,0 +1,107 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { alias, sort } from "@ember/object/computed";
import { next } from "@ember/runloop";
import Controller, { inject as controller } from "@ember/controller";
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import bootbox from "bootbox";
export default Controller.extend(GrantBadgeController, {
adminUser: controller(),
user: alias("adminUser.model"),
userBadges: alias("model"),
allBadges: alias("badges"),
sortedBadges: sort("model", "badgeSortOrder"),
init() {
this._super(...arguments);
this.badgeSortOrder = ["granted_at:desc"];
},
@discourseComputed("model", "model.[]", "model.expandedBadges.[]")
groupedBadges() {
const allBadges = this.model;
let grouped = {};
allBadges.forEach((b) => {
grouped[b.badge_id] = grouped[b.badge_id] || [];
grouped[b.badge_id].push(b);
});
let expanded = [];
const expandedBadges = allBadges.get("expandedBadges") || [];
Object.values(grouped).forEach(function (badges) {
let lastGranted = badges[0].granted_at;
badges.forEach((badge) => {
lastGranted =
lastGranted < badge.granted_at ? badge.granted_at : lastGranted;
});
if (badges.length === 1 || expandedBadges.includes(badges[0].badge.id)) {
badges.forEach((badge) => expanded.push(badge));
return;
}
let result = {
badge: badges[0].badge,
granted_at: lastGranted,
badges: badges,
count: badges.length,
grouped: true,
};
expanded.push(result);
});
return expanded.sortBy("granted_at").reverse();
},
actions: {
expandGroup: function (userBadge) {
const model = this.model;
model.set("expandedBadges", model.get("expandedBadges") || []);
model.get("expandedBadges").pushObject(userBadge.badge.id);
},
grantBadge() {
this.grantBadge(
this.selectedBadgeId,
this.get("user.username"),
this.badgeReason
).then(
() => {
this.set("badgeReason", "");
next(() => {
// Update the selected badge ID after the combobox has re-rendered.
const newSelectedBadge = this.grantableBadges[0];
if (newSelectedBadge) {
this.set("selectedBadgeId", newSelectedBadge.get("id"));
}
});
},
function (error) {
popupAjaxError(error);
}
);
},
revokeBadge(userBadge) {
return bootbox.confirm(
I18n.t("admin.badges.revoke_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
userBadge.revoke().then(() => {
this.model.removeObject(userBadge);
});
}
}
);
},
},
});
@@ -0,0 +1,72 @@
import I18n from "I18n";
import { gte, sort } from "@ember/object/computed";
import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import bootbox from "bootbox";
const MAX_FIELDS = 20;
export default Controller.extend({
fieldTypes: null,
createDisabled: gte("model.length", MAX_FIELDS),
sortedFields: sort("model", "fieldSortOrder"),
init() {
this._super(...arguments);
this.fieldSortOrder = ["position"];
},
actions: {
createField() {
const f = this.store.createRecord("user-field", {
field_type: "text",
position: MAX_FIELDS,
});
this.model.pushObject(f);
},
moveUp(f) {
const idx = this.sortedFields.indexOf(f);
if (idx) {
const prev = this.sortedFields.objectAt(idx - 1);
const prevPos = prev.get("position");
prev.update({ position: f.get("position") });
f.update({ position: prevPos });
}
},
moveDown(f) {
const idx = this.sortedFields.indexOf(f);
if (idx > -1) {
const next = this.sortedFields.objectAt(idx + 1);
const nextPos = next.get("position");
next.update({ position: f.get("position") });
f.update({ position: nextPos });
}
},
destroy(f) {
const model = this.model;
// Only confirm if we already been saved
if (f.get("id")) {
bootbox.confirm(I18n.t("admin.user_fields.delete_confirm"), function (
result
) {
if (result) {
f.destroyRecord()
.then(function () {
model.removeObject(f);
})
.catch(popupAjaxError);
}
});
} else {
model.removeObject(f);
}
},
},
});
@@ -0,0 +1,343 @@
import I18n from "I18n";
import { notEmpty, and } from "@ember/object/computed";
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import CanCheckEmails from "discourse/mixins/can-check-emails";
import { propertyNotEqual, setting } from "discourse/lib/computed";
import { userPath } from "discourse/lib/url";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import { htmlSafe } from "@ember/template";
import showModal from "discourse/lib/show-modal";
import bootbox from "bootbox";
export default Controller.extend(CanCheckEmails, {
adminTools: service(),
originalPrimaryGroupId: null,
customGroupIdsBuffer: null,
availableGroups: null,
userTitleValue: null,
showBadges: setting("enable_badges"),
hasLockedTrustLevel: notEmpty("model.manual_locked_trust_level"),
primaryGroupDirty: propertyNotEqual(
"originalPrimaryGroupId",
"model.primary_group_id"
),
canDisableSecondFactor: and(
"model.second_factor_enabled",
"model.can_disable_second_factor"
),
@discourseComputed("model.customGroups")
customGroupIds(customGroups) {
return customGroups.mapBy("id");
},
@discourseComputed("customGroupIdsBuffer", "customGroupIds")
customGroupsDirty(buffer, original) {
if (buffer === null) {
return false;
}
return buffer.length === original.length
? buffer.any((id) => !original.includes(id))
: true;
},
@discourseComputed("model.automaticGroups")
automaticGroups(automaticGroups) {
return automaticGroups
.map((group) => {
const name = htmlSafe(group.name);
return `<a href="/g/${name}">${name}</a>`;
})
.join(", ");
},
@discourseComputed("model.associated_accounts")
associatedAccountsLoaded(associatedAccounts) {
return typeof associatedAccounts !== "undefined";
},
@discourseComputed("model.associated_accounts")
associatedAccounts(associatedAccounts) {
return associatedAccounts
.map((provider) => `${provider.name} (${provider.description})`)
.join(", ");
},
@discourseComputed("model.user_fields.[]")
userFields(userFields) {
return this.site.collectUserFields(userFields);
},
preferencesPath: fmt("model.username_lower", userPath("%@/preferences")),
@discourseComputed(
"model.can_delete_all_posts",
"model.staff",
"model.post_count"
)
deleteAllPostsExplanation(canDeleteAllPosts, staff, postCount) {
if (canDeleteAllPosts) {
return null;
}
if (staff) {
return I18n.t("admin.user.delete_posts_forbidden_because_staff");
}
if (postCount > this.siteSettings.delete_all_posts_max) {
return I18n.t("admin.user.cant_delete_all_too_many_posts", {
count: this.siteSettings.delete_all_posts_max,
});
} else {
return I18n.t("admin.user.cant_delete_all_posts", {
count: this.siteSettings.delete_user_max_post_age,
});
}
},
@discourseComputed("model.canBeDeleted", "model.staff")
deleteExplanation(canBeDeleted, staff) {
if (canBeDeleted) {
return null;
}
if (staff) {
return I18n.t("admin.user.delete_forbidden_because_staff");
} else {
return I18n.t("admin.user.delete_forbidden", {
count: this.siteSettings.delete_user_max_post_age,
});
}
},
groupAdded(added) {
this.model
.groupAdded(added)
.catch(() => bootbox.alert(I18n.t("generic_error")));
},
groupRemoved(groupId) {
this.model
.groupRemoved(groupId)
.then(() => {
if (groupId === this.originalPrimaryGroupId) {
this.set("originalPrimaryGroupId", null);
}
})
.catch(() => bootbox.alert(I18n.t("generic_error")));
},
@discourseComputed("model.single_sign_on_record.last_payload")
ssoPayload(lastPayload) {
return lastPayload.split("&");
},
actions: {
impersonate() {
return this.model.impersonate();
},
logOut() {
return this.model.logOut();
},
resetBounceScore() {
return this.model.resetBounceScore();
},
approve() {
return this.model.approve(this.currentUser);
},
deactivate() {
return this.model.deactivate();
},
sendActivationEmail() {
return this.model.sendActivationEmail();
},
activate() {
return this.model.activate();
},
revokeAdmin() {
return this.model.revokeAdmin();
},
grantAdmin() {
return this.model.grantAdmin();
},
revokeModeration() {
return this.model.revokeModeration();
},
grantModeration() {
return this.model.grantModeration();
},
saveTrustLevel() {
return this.model.saveTrustLevel();
},
restoreTrustLevel() {
return this.model.restoreTrustLevel();
},
lockTrustLevel(locked) {
return this.model.lockTrustLevel(locked);
},
unsilence() {
return this.model.unsilence();
},
silence() {
return this.model.silence();
},
deleteAllPosts() {
return this.model.deleteAllPosts();
},
anonymize() {
return this.model.anonymize();
},
disableSecondFactor() {
return this.model.disableSecondFactor();
},
clearPenaltyHistory() {
const user = this.model;
const path = `/admin/users/${user.get("id")}/penalty_history`;
return ajax(path, { type: "DELETE" })
.then(() => user.set("tl3_requirements.penalty_counts.total", 0))
.catch(popupAjaxError);
},
destroy() {
const postCount = this.get("model.post_count");
const maxPostCount = this.siteSettings.delete_all_posts_max;
if (postCount <= maxPostCount) {
return this.model.destroy({ deletePosts: true });
} else {
return this.model.destroy();
}
},
promptTargetUser() {
showModal("admin-merge-users-prompt", {
admin: true,
model: this.model,
});
},
showMergeConfirmation(targetUsername) {
showModal("admin-merge-users-confirmation", {
admin: true,
model: {
username: this.model.username,
targetUsername,
},
});
},
merge(targetUsername) {
return this.model.merge({ targetUsername });
},
viewActionLogs() {
this.adminTools.showActionLogs(this, {
target_user: this.get("model.username"),
});
},
showSuspendModal() {
this.adminTools.showSuspendModal(this.model);
},
unsuspend() {
this.model.unsuspend().catch(popupAjaxError);
},
showSilenceModal() {
this.adminTools.showSilenceModal(this.model);
},
saveUsername(newUsername) {
const oldUsername = this.get("model.username");
this.set("model.username", newUsername);
const path = `/users/${oldUsername.toLowerCase()}/preferences/username`;
return ajax(path, { data: { new_username: newUsername }, type: "PUT" })
.catch((e) => {
this.set("model.username", oldUsername);
popupAjaxError(e);
})
.finally(() => this.toggleProperty("editingUsername"));
},
saveName(newName) {
const oldName = this.get("model.name");
this.set("model.name", newName);
const path = userPath(`${this.get("model.username").toLowerCase()}.json`);
return ajax(path, { data: { name: newName }, type: "PUT" })
.catch((e) => {
this.set("model.name", oldName);
popupAjaxError(e);
})
.finally(() => this.toggleProperty("editingName"));
},
saveTitle(newTitle) {
const oldTitle = this.get("model.title");
this.set("model.title", newTitle);
const path = userPath(`${this.get("model.username").toLowerCase()}.json`);
return ajax(path, { data: { title: newTitle }, type: "PUT" })
.catch((e) => {
this.set("model.title", oldTitle);
popupAjaxError(e);
})
.finally(() => this.toggleProperty("editingTitle"));
},
saveCustomGroups() {
const currentIds = this.customGroupIds;
const bufferedIds = this.customGroupIdsBuffer;
const availableGroups = this.availableGroups;
bufferedIds
.filter((id) => !currentIds.includes(id))
.forEach((id) => this.groupAdded(availableGroups.findBy("id", id)));
currentIds
.filter((id) => !bufferedIds.includes(id))
.forEach((id) => this.groupRemoved(id));
},
resetCustomGroups() {
this.set("customGroupIdsBuffer", this.model.customGroups.mapBy("id"));
},
savePrimaryGroup() {
const primaryGroupId = this.get("model.primary_group_id");
const path = `/admin/users/${this.get("model.id")}/primary_group`;
return ajax(path, {
type: "PUT",
data: { primary_group_id: primaryGroupId },
})
.then(() => this.set("originalPrimaryGroupId", primaryGroupId))
.catch(() => bootbox.alert(I18n.t("generic_error")));
},
resetPrimaryGroup() {
this.set("model.primary_group_id", this.originalPrimaryGroupId);
},
deleteSSORecord() {
return bootbox.confirm(
I18n.t("admin.user.sso.confirm_delete"),
I18n.t("no_value"),
I18n.t("yes_value"),
() => {
return this.model.deleteSSORecord();
}
);
},
},
});
@@ -0,0 +1,2 @@
import Controller from "@ember/controller";
export default Controller.extend();
@@ -0,0 +1,82 @@
import I18n from "I18n";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import discourseDebounce from "discourse/lib/debounce";
import { i18n } from "discourse/lib/computed";
import AdminUser from "admin/models/admin-user";
import CanCheckEmails from "discourse/mixins/can-check-emails";
import { INPUT_DELAY } from "discourse-common/config/environment";
export default Controller.extend(CanCheckEmails, {
model: null,
query: null,
order: null,
asc: null,
showEmails: false,
refreshing: false,
listFilter: null,
selectAll: false,
searchHint: i18n("search_hint"),
init() {
this._super(...arguments);
this._page = 1;
this._results = [];
this._canLoadMore = true;
},
@discourseComputed("query")
title(query) {
return I18n.t("admin.users.titles." + query);
},
@observes("listFilter")
_filterUsers: discourseDebounce(function () {
this.resetFilters();
}, INPUT_DELAY),
resetFilters() {
this._page = 1;
this._results = [];
this._canLoadMore = true;
this._refreshUsers();
},
_refreshUsers() {
if (!this._canLoadMore) {
return;
}
this.set("refreshing", true);
AdminUser.findAll(this.query, {
filter: this.listFilter,
show_emails: this.showEmails,
order: this.order,
asc: this.asc,
page: this._page,
})
.then((result) => {
if (!result || result.length === 0) {
this._canLoadMore = false;
}
this._results = this._results.concat(result);
this.set("model", this._results);
})
.finally(() => this.set("refreshing", false));
},
actions: {
loadMore() {
this._page += 1;
this._refreshUsers();
},
toggleEmailVisibility() {
this.toggleProperty("showEmails");
this.resetFilters();
},
},
});
@@ -0,0 +1,124 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { or } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import Controller, { inject as controller } from "@ember/controller";
import WatchedWord from "admin/models/watched-word";
import { ajax } from "discourse/lib/ajax";
import { fmt } from "discourse/lib/computed";
import showModal from "discourse/lib/show-modal";
import bootbox from "bootbox";
export default Controller.extend({
adminWatchedWords: controller(),
actionNameKey: null,
showWordsList: or(
"adminWatchedWords.filtered",
"adminWatchedWords.showWords"
),
downloadLink: fmt(
"actionNameKey",
"/admin/logs/watched_words/action/%@/download"
),
findAction(actionName) {
return (this.get("adminWatchedWords.model") || []).findBy(
"nameKey",
actionName
);
},
@discourseComputed("actionNameKey", "adminWatchedWords.model")
currentAction(actionName) {
return this.findAction(actionName);
},
@discourseComputed("currentAction.words.[]", "adminWatchedWords.model")
filteredContent(words) {
return words || [];
},
@discourseComputed("actionNameKey")
actionDescription(actionNameKey) {
return I18n.t("admin.watched_words.action_descriptions." + actionNameKey);
},
@discourseComputed("currentAction.count")
wordCount(count) {
return count || 0;
},
actions: {
recordAdded(arg) {
const a = this.findAction(this.actionNameKey);
if (a) {
a.words.unshiftObject(arg);
a.incrementProperty("count");
schedule("afterRender", () => {
// remove from other actions lists
let match = null;
this.get("adminWatchedWords.model").forEach((action) => {
if (match) {
return;
}
if (action.nameKey !== this.actionNameKey) {
match = action.words.findBy("id", arg.id);
if (match) {
action.words.removeObject(match);
action.decrementProperty("count");
}
}
});
});
}
},
recordRemoved(arg) {
if (this.currentAction) {
this.currentAction.words.removeObject(arg);
this.currentAction.decrementProperty("count");
}
},
uploadComplete() {
WatchedWord.findAll().then((data) => {
this.set("adminWatchedWords.model", data);
});
},
clearAll() {
const actionKey = this.actionNameKey;
bootbox.confirm(
I18n.t(`admin.watched_words.clear_all_confirm_${actionKey}`),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
ajax(`/admin/logs/watched_words/action/${actionKey}.json`, {
type: "DELETE",
}).then(() => {
const action = this.findAction(actionKey);
if (action) {
action.setProperties({
words: [],
count: 0,
});
}
});
}
}
);
},
test() {
WatchedWord.findAll().then((data) => {
this.set("adminWatchedWords.model", data);
showModal("admin-watched-word-test", {
admin: true,
model: this.currentAction,
});
});
},
},
});
@@ -0,0 +1,65 @@
import { isEmpty } from "@ember/utils";
import { alias } from "@ember/object/computed";
import EmberObject from "@ember/object";
import Controller from "@ember/controller";
import discourseDebounce from "discourse/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { INPUT_DELAY } from "discourse-common/config/environment";
export default Controller.extend({
filter: null,
filtered: false,
showWords: false,
disableShowWords: alias("filtered"),
regularExpressions: null,
filterContentNow() {
if (!!isEmpty(this.allWatchedWords)) {
return;
}
let filter;
if (this.filter) {
filter = this.filter.toLowerCase();
}
if (filter === undefined || filter.length < 1) {
this.set("model", this.allWatchedWords);
return;
}
const matchesByAction = [];
this.allWatchedWords.forEach((wordsForAction) => {
const wordRecords = wordsForAction.words.filter((wordRecord) => {
return wordRecord.word.indexOf(filter) > -1;
});
matchesByAction.pushObject(
EmberObject.create({
nameKey: wordsForAction.nameKey,
name: wordsForAction.name,
words: wordRecords,
count: wordRecords.length,
})
);
});
this.set("model", matchesByAction);
},
@observes("filter")
filterContent: discourseDebounce(function () {
this.filterContentNow();
this.set("filtered", !isEmpty(this.filter));
}, INPUT_DELAY),
actions: {
clearFilter() {
this.setProperties({ filter: "" });
},
toggleMenu() {
$(".admin-detail").toggleClass("mobile-closed mobile-open");
},
},
});
@@ -0,0 +1,80 @@
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
pingDisabled: false,
incomingCount: alias("incomingEventIds.length"),
init() {
this._super(...arguments);
this.incomingEventIds = [];
},
@discourseComputed("incomingCount")
hasIncoming(incomingCount) {
return incomingCount > 0;
},
subscribe() {
this.messageBus.subscribe(
`/web_hook_events/${this.get("model.extras.web_hook_id")}`,
(data) => {
if (data.event_type === "ping") {
this.set("pingDisabled", false);
}
this._addIncoming(data.web_hook_event_id);
}
);
},
unsubscribe() {
this.messageBus.unsubscribe("/web_hook_events/*");
},
_addIncoming(eventId) {
const incomingEventIds = this.incomingEventIds;
if (incomingEventIds.indexOf(eventId) === -1) {
incomingEventIds.pushObject(eventId);
}
},
actions: {
loadMore() {
this.model.loadMore();
},
ping() {
this.set("pingDisabled", true);
ajax(
`/admin/api/web_hooks/${this.get("model.extras.web_hook_id")}/ping`,
{
type: "POST",
}
).catch((error) => {
this.set("pingDisabled", false);
popupAjaxError(error);
});
},
showInserted() {
const webHookId = this.get("model.extras.web_hook_id");
ajax(`/admin/api/web_hooks/${webHookId}/events/bulk`, {
type: "GET",
data: { ids: this.incomingEventIds },
}).then((data) => {
const objects = data.map((event) =>
this.store.createRecord("web-hook-event", event)
);
this.model.unshiftObjects(objects);
this.set("incomingEventIds", []);
});
},
},
});
@@ -0,0 +1,151 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { extractDomainFromUrl } from "discourse/lib/utilities";
import EmberObject from "@ember/object";
import { isAbsoluteURL } from "discourse-common/lib/get-url";
import bootbox from "bootbox";
export default Controller.extend({
adminWebHooks: controller(),
eventTypes: alias("adminWebHooks.eventTypes"),
defaultEventTypes: alias("adminWebHooks.defaultEventTypes"),
contentTypes: alias("adminWebHooks.contentTypes"),
@discourseComputed
showTagsFilter() {
return this.siteSettings.tagging_enabled;
},
@discourseComputed("model.isSaving", "saved", "saveButtonDisabled")
savingStatus(isSaving, saved, saveButtonDisabled) {
if (isSaving) {
return I18n.t("saving");
} else if (!saveButtonDisabled && saved) {
return I18n.t("saved");
}
// Use side effect of validation to clear saved text
this.set("saved", false);
return "";
},
@discourseComputed("model.isNew")
saveButtonText(isNew) {
return isNew
? I18n.t("admin.web_hooks.create")
: I18n.t("admin.web_hooks.save");
},
@discourseComputed("model.secret")
secretValidation(secret) {
if (!isEmpty(secret)) {
if (secret.indexOf(" ") !== -1) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.secret_invalid"),
});
}
if (secret.length < 12) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.secret_too_short"),
});
}
}
},
@discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]")
eventTypeValidation(isWildcard, eventTypes) {
if (!isWildcard && isEmpty(eventTypes)) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.event_type_missing"),
});
}
},
@discourseComputed(
"model.isSaving",
"secretValidation",
"eventTypeValidation",
"model.payload_url"
)
saveButtonDisabled(
isSaving,
secretValidation,
eventTypeValidation,
payloadUrl
) {
return isSaving
? false
: secretValidation || eventTypeValidation || isEmpty(payloadUrl);
},
actions: {
save() {
this.set("saved", false);
const url = this.get("model.payload_url");
const domain = extractDomainFromUrl(url);
const model = this.model;
const isNew = model.get("isNew");
const saveWebHook = () => {
return model
.save()
.then(() => {
this.set("saved", true);
this.adminWebHooks.get("model").addObject(model);
if (isNew) {
this.transitionToRoute("adminWebHooks.show", model.get("id"));
}
})
.catch(popupAjaxError);
};
if (
domain === "localhost" ||
domain.match(/192\.168\.\d+\.\d+/) ||
domain.match(/127\.\d+\.\d+\.\d+/) ||
isAbsoluteURL(url)
) {
return bootbox.confirm(
I18n.t("admin.web_hooks.warn_local_payload_url"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
return saveWebHook();
}
}
);
}
return saveWebHook();
},
destroy() {
return bootbox.confirm(
I18n.t("admin.web_hooks.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
const model = this.model;
model
.destroyRecord()
.then(() => {
this.adminWebHooks.get("model").removeObject(model);
this.transitionToRoute("adminWebHooks");
})
.catch(popupAjaxError);
}
}
);
},
},
});
@@ -0,0 +1,30 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import bootbox from "bootbox";
export default Controller.extend({
actions: {
destroy(webhook) {
return bootbox.confirm(
I18n.t("admin.web_hooks.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
webhook
.destroyRecord()
.then(() => {
this.model.removeObject(webhook);
})
.catch(popupAjaxError);
}
}
);
},
loadMore() {
this.model.loadMore();
},
},
});
@@ -0,0 +1,41 @@
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
import { dasherize } from "@ember/string";
export default Controller.extend({
router: service(),
@discourseComputed("siteSettings.enable_group_directory")
showGroups(enableGroupDirectory) {
return !enableGroupDirectory;
},
@discourseComputed("siteSettings.enable_badges")
showBadges(enableBadges) {
return this.currentUser.get("admin") && enableBadges;
},
@discourseComputed("router._router.currentPath")
adminContentsClassName(currentPath) {
let cssClasses = currentPath
.split(".")
.filter((segment) => {
return (
segment !== "index" &&
segment !== "loading" &&
segment !== "show" &&
segment !== "admin"
);
})
.map(dasherize)
.join(" ");
// this is done to avoid breaking css customizations
if (cssClasses.includes("dashboard")) {
cssClasses = `${cssClasses} dashboard-next`;
}
return cssClasses;
},
});
@@ -0,0 +1,142 @@
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { and, not } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4];
const SCSS_VARIABLE_NAMES = [
// common/foundation/colors.scss
"primary",
"secondary",
"tertiary",
"quaternary",
"header_background",
"header_primary",
"highlight",
"danger",
"success",
"love",
// common/foundation/math.scss
"E",
"PI",
"LN2",
"SQRT2",
// common/foundation/variables.scss
"small-width",
"medium-width",
"large-width",
"google",
"instagram",
"facebook",
"cas",
"twitter",
"github",
"base-font-size",
"base-line-height",
"base-font-family",
"primary-low",
"primary-medium",
"secondary-low",
"secondary-medium",
"tertiary-low",
"quaternary-low",
"highlight-low",
"highlight-medium",
"danger-low",
"danger-medium",
"success-low",
"love-low",
];
export default Controller.extend(ModalFunctionality, {
adminCustomizeThemesShow: controller(),
uploadUrl: "/admin/themes/upload_asset",
onShow() {
this.set("name", null);
this.set("fileSelected", false);
},
enabled: and("nameValid", "fileSelected"),
disabled: not("enabled"),
@discourseComputed("name", "adminCustomizeThemesShow.model.theme_fields")
errorMessage(name, themeFields) {
if (name) {
if (!name.match(/^[a-z_][a-z0-9_-]*$/i)) {
return I18n.t(
"admin.customize.theme.variable_name_error.invalid_syntax"
);
} else if (SCSS_VARIABLE_NAMES.includes(name.toLowerCase())) {
return I18n.t("admin.customize.theme.variable_name_error.no_overwrite");
} else if (
themeFields.some(
(tf) =>
THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) &&
name === tf.name
)
) {
return I18n.t(
"admin.customize.theme.variable_name_error.must_be_unique"
);
}
}
return null;
},
@discourseComputed("errorMessage")
nameValid(errorMessage) {
return null === errorMessage;
},
@observes("name")
uploadChanged() {
const file = $("#file-input")[0];
this.set("fileSelected", file && file.files[0]);
},
actions: {
updateName() {
let name = this.name;
if (isEmpty(name)) {
name = $("#file-input")[0].files[0].name;
this.set("name", name.split(".")[0]);
}
this.uploadChanged();
},
upload() {
const file = $("#file-input")[0].files[0];
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData(),
};
options.data.append("file", file);
ajax(this.uploadUrl, options)
.then((result) => {
const upload = {
upload_id: result.upload_id,
name: this.name,
original_filename: file.name,
};
this.adminCustomizeThemesShow.send("addUpload", upload);
this.send("closeModal");
})
.catch((e) => {
popupAjaxError(e);
});
},
},
});
@@ -0,0 +1,59 @@
import I18n from "I18n";
import { alias, map } from "@ember/object/computed";
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import { escapeExpression } from "discourse/lib/utilities";
export default Controller.extend({
sample: alias("model.sample"),
errors: alias("model.errors"),
count: alias("model.grant_count"),
@discourseComputed("count", "sample.length")
countWarning(count, sampleLength) {
if (count <= 10) {
return sampleLength !== count;
} else {
return sampleLength !== 10;
}
},
@discourseComputed("model.query_plan")
hasQueryPlan(queryPlan) {
return !!queryPlan;
},
@discourseComputed("model.query_plan")
queryPlanHtml(queryPlan) {
let output = `<pre class="badge-query-plan">`;
queryPlan.forEach((linehash) => {
output += escapeExpression(linehash["QUERY PLAN"]);
output += "<br>";
});
output += "</pre>";
return output;
},
processedSample: map("model.sample", (grant) => {
let i18nKey = "admin.badges.preview.grant.with";
const i18nParams = { username: escapeExpression(grant.username) };
if (grant.post_id) {
i18nKey += "_post";
i18nParams.link = `<a href="/p/${grant.post_id}" data-auto-route="true">
${escapeExpression(grant.title)}
</a>`;
}
if (grant.granted_at) {
i18nKey += "_time";
i18nParams.time = escapeExpression(
moment(grant.granted_at).format(I18n.t("dates.long_with_year"))
);
}
return I18n.t(i18nKey, i18nParams);
}),
});
@@ -0,0 +1,27 @@
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
adminCustomizeColors: controller(),
selectedBaseThemeId: null,
init() {
this._super(...arguments);
const defaultScheme = this.get(
"adminCustomizeColors.baseColorSchemes.0.base_scheme_id"
);
this.set("selectedBaseThemeId", defaultScheme);
},
actions: {
selectBase() {
this.adminCustomizeColors.send(
"newColorSchemeWithBase",
this.selectedBaseThemeId
);
this.send("closeModal");
},
},
});
@@ -0,0 +1,85 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import { A } from "@ember/array";
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { observes } from "discourse-common/utils/decorators";
import bootbox from "bootbox";
export default Controller.extend(ModalFunctionality, {
@observes("model")
modelChanged() {
const model = this.model;
const copy = A();
const store = this.store;
if (model) {
model.forEach((o) =>
copy.pushObject(store.createRecord("badge-grouping", o))
);
}
this.set("workingCopy", copy);
},
moveItem(item, delta) {
const copy = this.workingCopy;
const index = copy.indexOf(item);
if (index + delta < 0 || index + delta >= copy.length) {
return;
}
copy.removeAt(index);
copy.insertAt(index + delta, item);
},
actions: {
up(item) {
this.moveItem(item, -1);
},
down(item) {
this.moveItem(item, 1);
},
delete(item) {
this.workingCopy.removeObject(item);
},
cancel() {
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
edit(item) {
item.set("editing", true);
},
save(item) {
item.set("editing", false);
},
add() {
const obj = this.store.createRecord("badge-grouping", {
editing: true,
name: I18n.t("admin.badges.badge_grouping"),
});
this.workingCopy.pushObject(obj);
},
saveAll() {
let items = this.workingCopy;
const groupIds = items.map((i) => i.get("id") || -1);
const names = items.map((i) => i.get("name"));
ajax("/admin/badges/badge_groupings", {
data: { ids: groupIds, names },
type: "POST",
}).then(
(data) => {
items = this.model;
items.clear();
data.badge_groupings.forEach((g) => {
items.pushObject(this.store.createRecord("badge-grouping", g));
});
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
() => bootbox.alert(I18n.t("generic_error"))
);
},
},
});
@@ -0,0 +1,26 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import IncomingEmail from "admin/models/incoming-email";
import { longDate } from "discourse/lib/formatter";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend(ModalFunctionality, {
@discourseComputed("model.date")
date(d) {
return longDate(d);
},
load(id) {
return IncomingEmail.find(id).then((result) => this.set("model", result));
},
loadFromBounced(id) {
return IncomingEmail.findByBounced(id)
.then((result) => this.set("model", result))
.catch((error) => {
this.send("closeModal");
popupAjaxError(error);
});
},
});
@@ -0,0 +1,199 @@
import I18n from "I18n";
import { equal, match, alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { THEMES, COMPONENTS } from "admin/models/theme";
import { POPULAR_THEMES } from "discourse-common/helpers/popular-themes";
import { set } from "@ember/object";
const MIN_NAME_LENGTH = 4;
export default Controller.extend(ModalFunctionality, {
adminCustomizeThemes: controller(),
themesController: controller("adminCustomizeThemes"),
popular: equal("selection", "popular"),
local: equal("selection", "local"),
remote: equal("selection", "remote"),
create: equal("selection", "create"),
selection: "popular",
loading: false,
keyGenUrl: "/admin/themes/generate_key_pair",
importUrl: "/admin/themes/import",
recordType: "theme",
checkPrivate: match("uploadUrl", /^git/),
localFile: null,
uploadUrl: null,
urlPlaceholder: "https://github.com/discourse/sample_theme",
advancedVisible: false,
selectedType: alias("themesController.currentTab"),
component: equal("selectedType", COMPONENTS),
init() {
this._super(...arguments);
this.createTypes = [
{ name: I18n.t("admin.customize.theme.theme"), value: THEMES },
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENTS },
];
},
@discourseComputed("themesController.installedThemes")
themes(installedThemes) {
return POPULAR_THEMES.map((t) => {
if (installedThemes.includes(t.name)) {
set(t, "installed", true);
}
return t;
});
},
@discourseComputed(
"loading",
"remote",
"uploadUrl",
"local",
"localFile",
"create",
"nameTooShort"
)
installDisabled(
isLoading,
isRemote,
uploadUrl,
isLocal,
localFile,
isCreate,
nameTooShort
) {
return (
isLoading ||
(isRemote && !uploadUrl) ||
(isLocal && !localFile) ||
(isCreate && nameTooShort)
);
},
@observes("privateChecked")
privateWasChecked() {
this.privateChecked
? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git")
: this.set("urlPlaceholder", "https://github.com/discourse/sample_theme");
const checked = this.privateChecked;
if (checked && !this._keyLoading) {
this._keyLoading = true;
ajax(this.keyGenUrl, { type: "POST" })
.then((pair) => {
this.setProperties({
privateKey: pair.private_key,
publicKey: pair.public_key,
});
})
.catch(popupAjaxError)
.finally(() => {
this._keyLoading = false;
});
}
},
@discourseComputed("name")
nameTooShort(name) {
return !name || name.length < MIN_NAME_LENGTH;
},
@discourseComputed("component")
placeholder(component) {
if (component) {
return I18n.t("admin.customize.theme.component_name");
} else {
return I18n.t("admin.customize.theme.theme_name");
}
},
@discourseComputed("selection")
submitLabel(selection) {
return `admin.customize.theme.${
selection === "create" ? "create" : "install"
}`;
},
@discourseComputed("privateChecked", "checkPrivate", "publicKey")
showPublicKey(privateChecked, checkPrivate, publicKey) {
return privateChecked && checkPrivate && publicKey;
},
actions: {
uploadLocaleFile() {
this.set("localFile", $("#file-input")[0].files[0]);
},
toggleAdvanced() {
this.toggleProperty("advancedVisible");
},
installThemeFromList(url) {
this.set("uploadUrl", url);
this.send("installTheme");
},
installTheme() {
if (this.create) {
this.set("loading", true);
const theme = this.store.createRecord(this.recordType);
theme
.save({ name: this.name, component: this.component })
.then(() => {
this.themesController.send("addTheme", theme);
this.send("closeModal");
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
return;
}
let options = {
type: "POST",
};
if (this.local) {
options.processData = false;
options.contentType = false;
options.data = new FormData();
options.data.append("theme", this.localFile);
}
if (this.remote || this.popular) {
options.data = {
remote: this.uploadUrl,
branch: this.branch,
};
if (this.privateChecked) {
options.data.private_key = this.privateKey;
}
}
if (this.get("model.user_id")) {
// Used by theme-creator
options.data["user_id"] = this.get("model.user_id");
}
this.set("loading", true);
ajax(this.importUrl, options)
.then((result) => {
const theme = this.store.createRecord(this.recordType, result.theme);
this.adminCustomizeThemes.send("addTheme", theme);
this.send("closeModal");
})
.then(() => {
this.setProperties({ privateKey: null, publicKey: null });
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
},
});
@@ -0,0 +1,40 @@
import I18n from "I18n";
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import { action } from "@ember/object";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
username: alias("model.username"),
targetUsername: alias("model.targetUsername"),
onShow() {
this.set("value", null);
},
@discourseComputed("username", "targetUsername")
text(username, targetUsername) {
return I18n.t(`admin.user.merge.confirmation.text`, {
username,
targetUsername,
});
},
@discourseComputed("value", "text")
mergeDisabled(value, text) {
return !value || text !== value;
},
@action
confirm() {
this.adminUserIndex.send("merge", this.targetUsername);
this.send("closeModal");
},
@action
close() {
this.send("closeModal");
},
});
@@ -0,0 +1,30 @@
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import { action } from "@ember/object";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
username: alias("model.username"),
onShow() {
this.set("targetUsername", null);
},
@discourseComputed("username", "targetUsername")
mergeDisabled(username, targetUsername) {
return !targetUsername || username === targetUsername;
},
@action
showConfirmation() {
this.send("closeModal");
this.adminUserIndex.send("showMergeConfirmation", this.targetUsername);
},
@action
close() {
this.send("closeModal");
},
});
@@ -0,0 +1,45 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
export default Controller.extend(ModalFunctionality, {
loading: true,
reseeding: false,
categories: null,
topics: null,
onShow() {
ajax("/admin/customize/reseed")
.then((result) => {
this.setProperties({
categories: result.categories,
topics: result.topics,
});
})
.finally(() => this.set("loading", false));
},
_extractSelectedIds(items) {
return items.filter((item) => item.selected).map((item) => item.id);
},
actions: {
reseed() {
this.set("reseeding", true);
ajax("/admin/customize/reseed", {
data: {
category_ids: this._extractSelectedIds(this.categories),
topic_ids: this._extractSelectedIds(this.topics),
},
type: "POST",
})
.then(
() => this.send("closeModal"),
() => bootbox.alert(I18n.t("generic_error"))
)
.finally(() => this.set("reseeding", false));
},
},
});
@@ -0,0 +1,39 @@
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import Controller from "@ember/controller";
import PenaltyController from "admin/mixins/penalty-controller";
export default Controller.extend(PenaltyController, {
silenceUntil: null,
silencing: false,
onShow() {
this.resetModal();
this.setProperties({ silenceUntil: null, silencing: false });
},
@discourseComputed("silenceUntil", "reason", "silencing")
submitDisabled(silenceUntil, reason, silencing) {
return silencing || isEmpty(silenceUntil) || !reason || reason.length < 1;
},
actions: {
silence() {
if (this.submitDisabled) {
return;
}
this.set("silencing", true);
this.penalize(() => {
return this.user.silence({
silenced_till: this.silenceUntil,
reason: this.reason,
message: this.message,
post_id: this.postId,
post_action: this.postAction,
post_edit: this.postEdit,
});
}).finally(() => this.set("silencing", false));
},
},
});
@@ -0,0 +1,4 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality);
@@ -0,0 +1,22 @@
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
adminBackupsLogs: controller(),
actions: {
startBackupWithUploads() {
this.send("closeModal");
this.send("startBackup", true);
},
startBackupWithoutUploads() {
this.send("closeModal");
this.send("startBackup", false);
},
cancel() {
this.send("closeModal");
},
},
});
@@ -0,0 +1,40 @@
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import Controller from "@ember/controller";
import PenaltyController from "admin/mixins/penalty-controller";
export default Controller.extend(PenaltyController, {
suspendUntil: null,
suspending: false,
onShow() {
this.resetModal();
this.setProperties({ suspendUntil: null, suspending: false });
},
@discourseComputed("suspendUntil", "reason", "suspending")
submitDisabled(suspendUntil, reason, suspending) {
return suspending || isEmpty(suspendUntil) || !reason || reason.length < 1;
},
actions: {
suspend() {
if (this.submitDisabled) {
return;
}
this.set("suspending", true);
this.penalize(() => {
return this.user.suspend({
suspend_until: this.suspendUntil,
reason: this.reason,
message: this.message,
post_id: this.postId,
post_action: this.postAction,
post_edit: this.postEdit,
});
}).finally(() => this.set("suspending", false));
},
},
});
@@ -0,0 +1,15 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
export default Controller.extend(ModalFunctionality, {
loadDiff() {
this.set("loading", true);
ajax(
"/admin/logs/staff_action_logs/" + this.get("model.id") + "/diff"
).then((diff) => {
this.set("loading", false);
this.set("diff", diff.side_by_side);
});
},
});
@@ -0,0 +1,27 @@
import Controller from "@ember/controller";
import { on, observes } from "discourse-common/utils/decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
@on("init")
@observes("model.value")
_setup() {
const value = this.get("model.value");
this.set("images", value && value.length ? value.split("\n") : []);
},
actions: {
uploadDone({ url }) {
this.images.addObject(url);
},
remove(url) {
this.images.removeObject(url);
},
close() {
this.save(this.images.join("\n"));
this.send("closeModal");
},
},
});
@@ -0,0 +1,14 @@
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
@discourseComputed("value", "model.compiledRegularExpression")
matches(value, regexpString) {
if (!value || !regexpString) {
return;
}
let censorRegexp = new RegExp(regexpString, "ig");
return value.match(censorRegexp);
},
});
@@ -0,0 +1,20 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
onShow() {
this.set("updateExistingUsers", null);
},
actions: {
updateExistingUsers() {
this.set("updateExistingUsers", true);
this.send("closeModal");
},
cancel() {
this.set("updateExistingUsers", false);
this.send("closeModal");
},
},
});