REFACTOR: Support bundling our admin section as an ember addon

This commit is contained in:
Robin Ward
2020-09-22 15:14:29 -04:00
parent cfb3f4db13
commit ce3fe2f4c4
448 changed files with 130 additions and 29 deletions
@@ -0,0 +1,142 @@
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { and, not } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4];
const SCSS_VARIABLE_NAMES = [
// common/foundation/colors.scss
"primary",
"secondary",
"tertiary",
"quaternary",
"header_background",
"header_primary",
"highlight",
"danger",
"success",
"love",
// common/foundation/math.scss
"E",
"PI",
"LN2",
"SQRT2",
// common/foundation/variables.scss
"small-width",
"medium-width",
"large-width",
"google",
"instagram",
"facebook",
"cas",
"twitter",
"github",
"base-font-size",
"base-line-height",
"base-font-family",
"primary-low",
"primary-medium",
"secondary-low",
"secondary-medium",
"tertiary-low",
"quaternary-low",
"highlight-low",
"highlight-medium",
"danger-low",
"danger-medium",
"success-low",
"love-low",
];
export default Controller.extend(ModalFunctionality, {
adminCustomizeThemesShow: controller(),
uploadUrl: "/admin/themes/upload_asset",
onShow() {
this.set("name", null);
this.set("fileSelected", false);
},
enabled: and("nameValid", "fileSelected"),
disabled: not("enabled"),
@discourseComputed("name", "adminCustomizeThemesShow.model.theme_fields")
errorMessage(name, themeFields) {
if (name) {
if (!name.match(/^[a-z_][a-z0-9_-]*$/i)) {
return I18n.t(
"admin.customize.theme.variable_name_error.invalid_syntax"
);
} else if (SCSS_VARIABLE_NAMES.includes(name.toLowerCase())) {
return I18n.t("admin.customize.theme.variable_name_error.no_overwrite");
} else if (
themeFields.some(
(tf) =>
THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) &&
name === tf.name
)
) {
return I18n.t(
"admin.customize.theme.variable_name_error.must_be_unique"
);
}
}
return null;
},
@discourseComputed("errorMessage")
nameValid(errorMessage) {
return null === errorMessage;
},
@observes("name")
uploadChanged() {
const file = $("#file-input")[0];
this.set("fileSelected", file && file.files[0]);
},
actions: {
updateName() {
let name = this.name;
if (isEmpty(name)) {
name = $("#file-input")[0].files[0].name;
this.set("name", name.split(".")[0]);
}
this.uploadChanged();
},
upload() {
const file = $("#file-input")[0].files[0];
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData(),
};
options.data.append("file", file);
ajax(this.uploadUrl, options)
.then((result) => {
const upload = {
upload_id: result.upload_id,
name: this.name,
original_filename: file.name,
};
this.adminCustomizeThemesShow.send("addUpload", upload);
this.send("closeModal");
})
.catch((e) => {
popupAjaxError(e);
});
},
},
});
@@ -0,0 +1,59 @@
import I18n from "I18n";
import { alias, map } from "@ember/object/computed";
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import { escapeExpression } from "discourse/lib/utilities";
export default Controller.extend({
sample: alias("model.sample"),
errors: alias("model.errors"),
count: alias("model.grant_count"),
@discourseComputed("count", "sample.length")
countWarning(count, sampleLength) {
if (count <= 10) {
return sampleLength !== count;
} else {
return sampleLength !== 10;
}
},
@discourseComputed("model.query_plan")
hasQueryPlan(queryPlan) {
return !!queryPlan;
},
@discourseComputed("model.query_plan")
queryPlanHtml(queryPlan) {
let output = `<pre class="badge-query-plan">`;
queryPlan.forEach((linehash) => {
output += escapeExpression(linehash["QUERY PLAN"]);
output += "<br>";
});
output += "</pre>";
return output;
},
processedSample: map("model.sample", (grant) => {
let i18nKey = "admin.badges.preview.grant.with";
const i18nParams = { username: escapeExpression(grant.username) };
if (grant.post_id) {
i18nKey += "_post";
i18nParams.link = `<a href="/p/${grant.post_id}" data-auto-route="true">
${escapeExpression(grant.title)}
</a>`;
}
if (grant.granted_at) {
i18nKey += "_time";
i18nParams.time = escapeExpression(
moment(grant.granted_at).format(I18n.t("dates.long_with_year"))
);
}
return I18n.t(i18nKey, i18nParams);
}),
});
@@ -0,0 +1,27 @@
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
adminCustomizeColors: controller(),
selectedBaseThemeId: null,
init() {
this._super(...arguments);
const defaultScheme = this.get(
"adminCustomizeColors.baseColorSchemes.0.base_scheme_id"
);
this.set("selectedBaseThemeId", defaultScheme);
},
actions: {
selectBase() {
this.adminCustomizeColors.send(
"newColorSchemeWithBase",
this.selectedBaseThemeId
);
this.send("closeModal");
},
},
});
@@ -0,0 +1,85 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import { A } from "@ember/array";
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { observes } from "discourse-common/utils/decorators";
import bootbox from "bootbox";
export default Controller.extend(ModalFunctionality, {
@observes("model")
modelChanged() {
const model = this.model;
const copy = A();
const store = this.store;
if (model) {
model.forEach((o) =>
copy.pushObject(store.createRecord("badge-grouping", o))
);
}
this.set("workingCopy", copy);
},
moveItem(item, delta) {
const copy = this.workingCopy;
const index = copy.indexOf(item);
if (index + delta < 0 || index + delta >= copy.length) {
return;
}
copy.removeAt(index);
copy.insertAt(index + delta, item);
},
actions: {
up(item) {
this.moveItem(item, -1);
},
down(item) {
this.moveItem(item, 1);
},
delete(item) {
this.workingCopy.removeObject(item);
},
cancel() {
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
edit(item) {
item.set("editing", true);
},
save(item) {
item.set("editing", false);
},
add() {
const obj = this.store.createRecord("badge-grouping", {
editing: true,
name: I18n.t("admin.badges.badge_grouping"),
});
this.workingCopy.pushObject(obj);
},
saveAll() {
let items = this.workingCopy;
const groupIds = items.map((i) => i.get("id") || -1);
const names = items.map((i) => i.get("name"));
ajax("/admin/badges/badge_groupings", {
data: { ids: groupIds, names },
type: "POST",
}).then(
(data) => {
items = this.model;
items.clear();
data.badge_groupings.forEach((g) => {
items.pushObject(this.store.createRecord("badge-grouping", g));
});
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
() => bootbox.alert(I18n.t("generic_error"))
);
},
},
});
@@ -0,0 +1,26 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import IncomingEmail from "admin/models/incoming-email";
import { longDate } from "discourse/lib/formatter";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend(ModalFunctionality, {
@discourseComputed("model.date")
date(d) {
return longDate(d);
},
load(id) {
return IncomingEmail.find(id).then((result) => this.set("model", result));
},
loadFromBounced(id) {
return IncomingEmail.findByBounced(id)
.then((result) => this.set("model", result))
.catch((error) => {
this.send("closeModal");
popupAjaxError(error);
});
},
});
@@ -0,0 +1,199 @@
import I18n from "I18n";
import { equal, match, alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { THEMES, COMPONENTS } from "admin/models/theme";
import { POPULAR_THEMES } from "discourse-common/helpers/popular-themes";
import { set } from "@ember/object";
const MIN_NAME_LENGTH = 4;
export default Controller.extend(ModalFunctionality, {
adminCustomizeThemes: controller(),
themesController: controller("adminCustomizeThemes"),
popular: equal("selection", "popular"),
local: equal("selection", "local"),
remote: equal("selection", "remote"),
create: equal("selection", "create"),
selection: "popular",
loading: false,
keyGenUrl: "/admin/themes/generate_key_pair",
importUrl: "/admin/themes/import",
recordType: "theme",
checkPrivate: match("uploadUrl", /^git/),
localFile: null,
uploadUrl: null,
urlPlaceholder: "https://github.com/discourse/sample_theme",
advancedVisible: false,
selectedType: alias("themesController.currentTab"),
component: equal("selectedType", COMPONENTS),
init() {
this._super(...arguments);
this.createTypes = [
{ name: I18n.t("admin.customize.theme.theme"), value: THEMES },
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENTS },
];
},
@discourseComputed("themesController.installedThemes")
themes(installedThemes) {
return POPULAR_THEMES.map((t) => {
if (installedThemes.includes(t.name)) {
set(t, "installed", true);
}
return t;
});
},
@discourseComputed(
"loading",
"remote",
"uploadUrl",
"local",
"localFile",
"create",
"nameTooShort"
)
installDisabled(
isLoading,
isRemote,
uploadUrl,
isLocal,
localFile,
isCreate,
nameTooShort
) {
return (
isLoading ||
(isRemote && !uploadUrl) ||
(isLocal && !localFile) ||
(isCreate && nameTooShort)
);
},
@observes("privateChecked")
privateWasChecked() {
this.privateChecked
? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git")
: this.set("urlPlaceholder", "https://github.com/discourse/sample_theme");
const checked = this.privateChecked;
if (checked && !this._keyLoading) {
this._keyLoading = true;
ajax(this.keyGenUrl, { type: "POST" })
.then((pair) => {
this.setProperties({
privateKey: pair.private_key,
publicKey: pair.public_key,
});
})
.catch(popupAjaxError)
.finally(() => {
this._keyLoading = false;
});
}
},
@discourseComputed("name")
nameTooShort(name) {
return !name || name.length < MIN_NAME_LENGTH;
},
@discourseComputed("component")
placeholder(component) {
if (component) {
return I18n.t("admin.customize.theme.component_name");
} else {
return I18n.t("admin.customize.theme.theme_name");
}
},
@discourseComputed("selection")
submitLabel(selection) {
return `admin.customize.theme.${
selection === "create" ? "create" : "install"
}`;
},
@discourseComputed("privateChecked", "checkPrivate", "publicKey")
showPublicKey(privateChecked, checkPrivate, publicKey) {
return privateChecked && checkPrivate && publicKey;
},
actions: {
uploadLocaleFile() {
this.set("localFile", $("#file-input")[0].files[0]);
},
toggleAdvanced() {
this.toggleProperty("advancedVisible");
},
installThemeFromList(url) {
this.set("uploadUrl", url);
this.send("installTheme");
},
installTheme() {
if (this.create) {
this.set("loading", true);
const theme = this.store.createRecord(this.recordType);
theme
.save({ name: this.name, component: this.component })
.then(() => {
this.themesController.send("addTheme", theme);
this.send("closeModal");
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
return;
}
let options = {
type: "POST",
};
if (this.local) {
options.processData = false;
options.contentType = false;
options.data = new FormData();
options.data.append("theme", this.localFile);
}
if (this.remote || this.popular) {
options.data = {
remote: this.uploadUrl,
branch: this.branch,
};
if (this.privateChecked) {
options.data.private_key = this.privateKey;
}
}
if (this.get("model.user_id")) {
// Used by theme-creator
options.data["user_id"] = this.get("model.user_id");
}
this.set("loading", true);
ajax(this.importUrl, options)
.then((result) => {
const theme = this.store.createRecord(this.recordType, result.theme);
this.adminCustomizeThemes.send("addTheme", theme);
this.send("closeModal");
})
.then(() => {
this.setProperties({ privateKey: null, publicKey: null });
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
},
});
@@ -0,0 +1,40 @@
import I18n from "I18n";
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import { action } from "@ember/object";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
username: alias("model.username"),
targetUsername: alias("model.targetUsername"),
onShow() {
this.set("value", null);
},
@discourseComputed("username", "targetUsername")
text(username, targetUsername) {
return I18n.t(`admin.user.merge.confirmation.text`, {
username,
targetUsername,
});
},
@discourseComputed("value", "text")
mergeDisabled(value, text) {
return !value || text !== value;
},
@action
confirm() {
this.adminUserIndex.send("merge", this.targetUsername);
this.send("closeModal");
},
@action
close() {
this.send("closeModal");
},
});
@@ -0,0 +1,30 @@
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import { action } from "@ember/object";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
username: alias("model.username"),
onShow() {
this.set("targetUsername", null);
},
@discourseComputed("username", "targetUsername")
mergeDisabled(username, targetUsername) {
return !targetUsername || username === targetUsername;
},
@action
showConfirmation() {
this.send("closeModal");
this.adminUserIndex.send("showMergeConfirmation", this.targetUsername);
},
@action
close() {
this.send("closeModal");
},
});
@@ -0,0 +1,45 @@
import I18n from "I18n";
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
export default Controller.extend(ModalFunctionality, {
loading: true,
reseeding: false,
categories: null,
topics: null,
onShow() {
ajax("/admin/customize/reseed")
.then((result) => {
this.setProperties({
categories: result.categories,
topics: result.topics,
});
})
.finally(() => this.set("loading", false));
},
_extractSelectedIds(items) {
return items.filter((item) => item.selected).map((item) => item.id);
},
actions: {
reseed() {
this.set("reseeding", true);
ajax("/admin/customize/reseed", {
data: {
category_ids: this._extractSelectedIds(this.categories),
topic_ids: this._extractSelectedIds(this.topics),
},
type: "POST",
})
.then(
() => this.send("closeModal"),
() => bootbox.alert(I18n.t("generic_error"))
)
.finally(() => this.set("reseeding", false));
},
},
});
@@ -0,0 +1,39 @@
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import Controller from "@ember/controller";
import PenaltyController from "admin/mixins/penalty-controller";
export default Controller.extend(PenaltyController, {
silenceUntil: null,
silencing: false,
onShow() {
this.resetModal();
this.setProperties({ silenceUntil: null, silencing: false });
},
@discourseComputed("silenceUntil", "reason", "silencing")
submitDisabled(silenceUntil, reason, silencing) {
return silencing || isEmpty(silenceUntil) || !reason || reason.length < 1;
},
actions: {
silence() {
if (this.submitDisabled) {
return;
}
this.set("silencing", true);
this.penalize(() => {
return this.user.silence({
silenced_till: this.silenceUntil,
reason: this.reason,
message: this.message,
post_id: this.postId,
post_action: this.postAction,
post_edit: this.postEdit,
});
}).finally(() => this.set("silencing", false));
},
},
});
@@ -0,0 +1,4 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality);
@@ -0,0 +1,22 @@
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
adminBackupsLogs: controller(),
actions: {
startBackupWithUploads() {
this.send("closeModal");
this.send("startBackup", true);
},
startBackupWithoutUploads() {
this.send("closeModal");
this.send("startBackup", false);
},
cancel() {
this.send("closeModal");
},
},
});
@@ -0,0 +1,40 @@
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import Controller from "@ember/controller";
import PenaltyController from "admin/mixins/penalty-controller";
export default Controller.extend(PenaltyController, {
suspendUntil: null,
suspending: false,
onShow() {
this.resetModal();
this.setProperties({ suspendUntil: null, suspending: false });
},
@discourseComputed("suspendUntil", "reason", "suspending")
submitDisabled(suspendUntil, reason, suspending) {
return suspending || isEmpty(suspendUntil) || !reason || reason.length < 1;
},
actions: {
suspend() {
if (this.submitDisabled) {
return;
}
this.set("suspending", true);
this.penalize(() => {
return this.user.suspend({
suspend_until: this.suspendUntil,
reason: this.reason,
message: this.message,
post_id: this.postId,
post_action: this.postAction,
post_edit: this.postEdit,
});
}).finally(() => this.set("suspending", false));
},
},
});
@@ -0,0 +1,15 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
export default Controller.extend(ModalFunctionality, {
loadDiff() {
this.set("loading", true);
ajax(
"/admin/logs/staff_action_logs/" + this.get("model.id") + "/diff"
).then((diff) => {
this.set("loading", false);
this.set("diff", diff.side_by_side);
});
},
});
@@ -0,0 +1,27 @@
import Controller from "@ember/controller";
import { on, observes } from "discourse-common/utils/decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
@on("init")
@observes("model.value")
_setup() {
const value = this.get("model.value");
this.set("images", value && value.length ? value.split("\n") : []);
},
actions: {
uploadDone({ url }) {
this.images.addObject(url);
},
remove(url) {
this.images.removeObject(url);
},
close() {
this.save(this.images.join("\n"));
this.send("closeModal");
},
},
});
@@ -0,0 +1,14 @@
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
@discourseComputed("value", "model.compiledRegularExpression")
matches(value, regexpString) {
if (!value || !regexpString) {
return;
}
let censorRegexp = new RegExp(regexpString, "ig");
return value.match(censorRegexp);
},
});
@@ -0,0 +1,20 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
onShow() {
this.set("updateExistingUsers", null);
},
actions: {
updateExistingUsers() {
this.set("updateExistingUsers", true);
this.send("closeModal");
},
cancel() {
this.set("updateExistingUsers", false);
this.send("closeModal");
},
},
});