REFACTOR: Support bundling our admin section as an ember addon
This commit is contained in:
@@ -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));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
+62
@@ -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);
|
||||
}),
|
||||
});
|
||||
+27
@@ -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));
|
||||
},
|
||||
},
|
||||
});
|
||||
+40
@@ -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));
|
||||
},
|
||||
},
|
||||
});
|
||||
+4
@@ -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);
|
||||
},
|
||||
});
|
||||
+20
@@ -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");
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user