@@ -0,0 +1,73 @@
import { action, computed } from "@ember/object";
import loadScript, { loadCSS } from "discourse/lib/load-script";
import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators";
import { schedule } from "@ember/runloop";

/**
An input field for a color.
@param hexValue is a reference to the color's hex value.
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor.
@params valid is a boolean indicating if the input field is a valid color.
**/
export default Component.extend({
classNames: ["color-picker"],

onlyHex: true,

styleSelection: true,

maxlength: computed("onlyHex", function () {
return this.onlyHex ? 6 : null;
}),

@action
onHexInput(color) {
this.attrs.onChangeColor && this.attrs.onChangeColor(color || "");
},

@observes("hexValue", "brightnessValue", "valid")
hexValueChanged: function () {
const hex = this.hexValue;
let text = this.element.querySelector("input.hex-input");

this.attrs.onChangeColor && this.attrs.onChangeColor(hex);

if (this.valid) {
this.styleSelection &&
text.setAttribute(
"style",
"color: " +
(this.brightnessValue > 125 ? "black" : "white") +
"; background-color: #" +
hex +
";"
);

if (this.pickerLoaded) {
$(this.element.querySelector(".picker")).spectrum({
color: "#" + hex,
});
}
} else {
this.styleSelection && text.setAttribute("style", "");
}
},

didInsertElement() {
loadScript("/javascripts/spectrum.js").then(() => {
loadCSS("/javascripts/spectrum.css").then(() => {
schedule("afterRender", () => {
$(this.element.querySelector(".picker"))
.spectrum({ color: "#" + this.hexValue })
.on("change.spectrum", (me, color) => {
this.set("hexValue", color.toHexString().replace("#", ""));
});
this.set("pickerLoaded", true);
});
});
});
schedule("afterRender", () => this.hexValueChanged());
},
});
@@ -0,0 +1,33 @@
import Component from "@ember/component";
import { action, computed } from "@ember/object";
import { ajax } from "discourse/lib/ajax";

export default Component.extend({
newFeatures: null,
classNames: ["section", "dashboard-new-features"],
classNameBindings: ["hasUnseenFeatures:ordered-first"],
releaseNotesLink: null,

init() {
this._super(...arguments);

ajax("/admin/dashboard/new-features.json").then((json) => {
this.setProperties({
newFeatures: json.new_features,
hasUnseenFeatures: json.has_unseen_features,
releaseNotesLink: json.release_notes_link,
});
});
},

columnCountClass: computed("newFeatures", function () {
return this.newFeatures.length > 2 ? "three-or-more-items" : "";
}),

@action
dismissNewFeatures() {
ajax("/admin/dashboard/mark-new-features-as-seen.json", {
type: "PUT",
}).then(() => this.set("hasUnseenFeatures", false));
},
});
@@ -0,0 +1,54 @@
import Component from "@ember/component";
import I18n from "I18n";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";

export default Component.extend({
editorId: reads("fieldName"),

@discourseComputed("fieldName")
currentEditorMode(fieldName) {
return fieldName === "css" ? "scss" : fieldName;
},

@discourseComputed("fieldName", "styles.html", "styles.css")
resetDisabled(fieldName) {
return (
this.get(`styles.${fieldName}`) ===
this.get(`styles.default_${fieldName}`)
);
},

@discourseComputed("styles", "fieldName")
editorContents: {
get(styles, fieldName) {
return styles[fieldName];
},
set(value, styles, fieldName) {
styles.setField(fieldName, value);
return value;
},
},

actions: {
reset() {
bootbox.confirm(
I18n.t("admin.customize.email_style.reset_confirm", {
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.styles.setField(
this.fieldName,
this.styles.get(`default_${this.fieldName}`)
);
this.notifyPropertyChange("editorContents");
}
}
);
},
},
});
@@ -0,0 +1,72 @@
import Category from "discourse/models/category";
import Component from "@ember/component";
import I18n from "I18n";
import bootbox from "bootbox";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { or } from "@ember/object/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";

export default Component.extend(bufferedProperty("host"), {
editToggled: false,
tagName: "tr",
categoryId: null,

editing: or("host.isNew", "editToggled"),

@discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) {
return isSaving || isEmpty(host);
},

actions: {
edit() {
this.set("categoryId", this.get("host.category.id"));
this.set("editToggled", true);
},

save() {
if (this.cantSave) {
return;
}

const props = this.buffered.getProperties(
"host",
"allowed_paths",
"class_name"
);
props.category_id = this.categoryId;

const host = this.host;

host
.save(props)
.then(() => {
host.set("category", Category.findById(this.categoryId));
this.set("editToggled", false);
})
.catch(popupAjaxError);
},

delete() {
bootbox.confirm(I18n.t("admin.embedding.confirm_delete"), (result) => {
if (result) {
this.host.destroyRecord().then(() => {
this.deleteHost(this.host);
});
}
});
},

cancel() {
const host = this.host;
if (host.get("isNew")) {
this.deleteHost(host);
} else {
this.rollbackBuffer();
this.set("editToggled", false);
}
},
},
});
@@ -0,0 +1,32 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";

export default Component.extend({
classNames: ["embed-setting"],

@discourseComputed("field")
inputId(field) {
return field.dasherize();
},

@discourseComputed("field")
translationKey(field) {
return `admin.embedding.${field}`;
},

@discourseComputed("type")
isCheckbox(type) {
return type === "checkbox";
},

@discourseComputed("value")
checked: {
get(value) {
return !!value;
},
set(value) {
this.set("value", value);
return value;
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["flag-user-lists"],
});
@@ -0,0 +1,11 @@
import { observes, on } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import highlightSyntax from "discourse/lib/highlight-syntax";

export default Component.extend({
@on("didInsertElement")
@observes("code")
_refresh() {
highlightSyntax(this.element, this.siteSettings, this.session);
},
});
@@ -0,0 +1,44 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";

export default Component.extend({
classNames: ["inline-edit"],

buffer: null,
bufferModelId: null,

didReceiveAttrs() {
this._super(...arguments);

if (this.modelId !== this.bufferModelId) {
// HACK: The condition above ensures this method is called only when its
// attributes are changed (i.e. only when `checked` changes).
//
// Reproduction steps: navigate to theme #1, switch to theme #2 from the
// left-side panel, then switch back to theme #1 and click on the <input>
// element wrapped by this component. It will call `didReceiveAttrs` even
// though none of the attributes have changed (only `buffer` does).
this.setProperties({
buffer: this.checked,
bufferModelId: this.modelId,
});
}
},

@discourseComputed("checked", "buffer")
changed(checked, buffer) {
return !!checked !== !!buffer;
},

@action
apply() {
this.set("checked", this.buffer);
this.action();
},

@action
cancel() {
this.set("buffer", this.checked);
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["install-theme-item"],
});
@@ -0,0 +1,117 @@
import AdminUser from "admin/models/admin-user";
import Component from "@ember/component";
import EmberObject from "@ember/object";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import copyText from "discourse/lib/copy-text";
import discourseComputed from "discourse-common/utils/decorators";
import { later } from "@ember/runloop";

export default Component.extend({
classNames: ["ip-lookup"],

@discourseComputed("other_accounts.length", "totalOthersWithSameIP")
otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) {
// can only delete up to 50 accounts at a time
const total = Math.min(50, totalOthersWithSameIP || 0);
const visible = Math.min(50, otherAccountsLength || 0);
return Math.max(visible, total);
},

actions: {
lookup() {
this.set("show", true);

if (!this.location) {
ajax("/admin/users/ip-info", {
data: { ip: this.ip },
}).then((location) =>
this.set("location", EmberObject.create(location))
);
}

if (!this.other_accounts) {
this.set("otherAccountsLoading", true);

const data = {
ip: this.ip,
exclude: this.userId,
order: "trust_level DESC",
};

ajax("/admin/users/total-others-with-same-ip", {
data,
}).then((result) => this.set("totalOthersWithSameIP", result.total));

AdminUser.findAll("active", data).then((users) => {
this.setProperties({
other_accounts: users,
otherAccountsLoading: false,
});
});
}
},

hide() {
this.set("show", false);
},

copy() {
let text = `IP: ${this.ip}\n`;
const location = this.location;
if (location) {
if (location.hostname) {
text += `${I18n.t("ip_lookup.hostname")}: ${location.hostname}\n`;
}

text += I18n.t("ip_lookup.location");
if (location.location) {
text += `: ${location.location}\n`;
} else {
text += `: ${I18n.t("ip_lookup.location_not_found")}\n`;
}

if (location.organization) {
text += I18n.t("ip_lookup.organisation");
text += `: ${location.organization}\n`;
}
}

const $copyRange = $('<p id="copy-range"></p>');
$copyRange.html(text.trim().replace(/\n/g, "<br>"));
$(document.body).append($copyRange);
if (copyText(text, $copyRange[0])) {
this.set("copied", true);
later(() => this.set("copied", false), 2000);
}
$copyRange.remove();
},

deleteOtherAccounts() {
bootbox.confirm(
I18n.t("ip_lookup.confirm_delete_other_accounts"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
this.setProperties({
other_accounts: null,
otherAccountsLoading: true,
totalOthersWithSameIP: null,
});

ajax("/admin/users/delete-others-with-same-ip.json", {
type: "DELETE",
data: {
ip: this.ip,
exclude: this.userId,
order: "trust_level DESC",
},
}).then(() => this.send("lookup"));
}
}
);
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});
@@ -0,0 +1,42 @@
import discourseComputed, {
afterRender,
} from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import { equal } from "@ember/object/computed";

const ACTIONS = ["delete", "delete_replies", "edit", "none"];

export default Component.extend({
postId: null,
postAction: null,
postEdit: null,

@discourseComputed
penaltyActions() {
return ACTIONS.map((id) => {
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
});
},

editing: equal("postAction", "edit"),

actions: {
penaltyChanged(postAction) {
this.set("postAction", postAction);

// If we switch to edit mode, jump to the edit textarea
if (postAction === "edit") {
this._focusEditTextarea();
}
},
},

@afterRender
_focusEditTextarea() {
const elem = this.element;
const body = elem.closest(".modal-body");
body.scrollTo(0, body.clientHeight);
elem.querySelector(".post-editor").focus();
},
});
@@ -0,0 +1,89 @@
import Component from "@ember/component";
import I18n from "I18n";
import Permalink from "admin/models/permalink";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import { schedule } from "@ember/runloop";

export default Component.extend({
classNames: ["permalink-form"],
formSubmitted: false,
permalinkType: "topic_id",
permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"),

@discourseComputed
permalinkTypes() {
return [
{ id: "topic_id", name: I18n.t("admin.permalink.topic_id") },
{ id: "post_id", name: I18n.t("admin.permalink.post_id") },
{ id: "category_id", name: I18n.t("admin.permalink.category_id") },
{ id: "tag_name", name: I18n.t("admin.permalink.tag_name") },
{ id: "external_url", name: I18n.t("admin.permalink.external_url") },
];
},

didInsertElement() {
this._super(...arguments);

schedule("afterRender", () => {
$(this.element.querySelector(".external-url")).keydown((e) => {
// enter key
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},

focusPermalink() {
schedule("afterRender", () =>
this.element.querySelector(".permalink-url").focus()
);
},

actions: {
submit() {
if (!this.formSubmitted) {
this.set("formSubmitted", true);

Permalink.create({
url: this.url,
permalink_type: this.permalinkType,
permalink_type_value: this.permalink_type_value,
})
.save()
.then(
(result) => {
this.setProperties({
url: "",
permalink_type_value: "",
formSubmitted: false,
});

this.action(Permalink.create(result.permalink));

this.focusPermalink();
},
(e) => {
this.set("formSubmitted", false);

let error;
if (e.responseJSON && e.responseJSON.errors) {
error = I18n.t("generic_error_with_reason", {
error: e.responseJSON.errors.join(". "),
});
} else {
error = I18n.t("generic_error");
}
bootbox.alert(error, () => this.focusPermalink());
}
);
}
},

onChangePermalinkType(type) {
this.set("permalinkType", type);
},
},
});
@@ -0,0 +1,16 @@
import FilterComponent from "admin/components/report-filters/filter";
import { action } from "@ember/object";

export default FilterComponent.extend({
checked: false,

didReceiveAttrs() {
this._super(...arguments);
this.set("checked", !!this.filter.default);
},

@action
onChange() {
this.applyFilter(this.filter.id, !this.checked || undefined);
},
});
@@ -0,0 +1,12 @@
import FilterComponent from "admin/components/report-filters/filter";
import { action } from "@ember/object";
import { readOnly } from "@ember/object/computed";

export default FilterComponent.extend({
category: readOnly("filter.default"),

@action
onChange(categoryId) {
this.applyFilter(this.filter.id, categoryId || undefined);
},
});
@@ -0,0 +1,9 @@
import Component from "@ember/component";
import { action } from "@ember/object";

export default Component.extend({
@action
onChange(value) {
this.applyFilter(this.filter.id, value);
},
});
@@ -0,0 +1,18 @@
import FilterComponent from "admin/components/report-filters/filter";
import { computed } from "@ember/object";

export default FilterComponent.extend({
classNames: ["group-filter"],

@computed
get groupOptions() {
return (this.site.groups || []).map((group) => {
return { name: group["name"], value: group["id"] };
});
},

@computed("filter.default")
get groupId() {
return this.filter.default ? parseInt(this.filter.default, 10) : null;
},
});
@@ -0,0 +1,3 @@
import FilterComponent from "admin/components/report-filters/filter";

export default FilterComponent.extend();
@@ -0,0 +1,140 @@
import discourseComputed, { on } from "discourse-common/utils/decorators";
import { later, schedule } from "@ember/runloop";
import Component from "@ember/component";
import I18n from "I18n";
import getURL from "discourse-common/lib/get-url";
import { iconHTML } from "discourse-common/lib/icon-library";

/*global Resumable:true */

/**
Example usage:
{{resumable-upload
target="/admin/backups/upload"
success=(action "successAction")
error=(action "errorAction")
uploadText="UPLOAD"
}}
**/
export default Component.extend({
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
isUploading: false,
progress: 0,
rerenderTriggers: ["isUploading", "progress"],
uploadingIcon: null,
progressBar: null,

@on("init")
_initialize() {
this.resumable = new Resumable({
target: getURL(this.target),
maxFiles: 1, // only 1 file at a time
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")
.content,
},
});

this.resumable.on("fileAdded", () => {
// automatically upload the selected file
this.resumable.upload();

// mark as uploading
later(() => {
this.set("isUploading", true);
this._updateIcon();
});
});

this.resumable.on("fileProgress", (file) => {
// update progress
later(() => {
this.set("progress", parseInt(file.progress() * 100, 10));
this._updateProgressBar();
});
});

this.resumable.on("fileSuccess", (file) => {
later(() => {
// mark as not uploading anymore
this._reset();

// fire an event to allow the parent route to reload its model
this.success(file.fileName);
});
});

this.resumable.on("fileError", (file, message) => {
later(() => {
// mark as not uploading anymore
this._reset();

// fire an event to allow the parent route to display the error message
this.error(file.fileName, message);
});
});
},

@on("didInsertElement")
_assignBrowse() {
schedule("afterRender", () => this.resumable.assignBrowse($(this.element)));
},

@on("willDestroyElement")
_teardown() {
if (this.resumable) {
this.resumable.cancel();
this.resumable = null;
}
},

@discourseComputed("title", "text")
translatedTitle(title, text) {
return title ? I18n.t(title) : text;
},

@discourseComputed("isUploading", "progress")
text(isUploading, progress) {
if (isUploading) {
return progress + " %";
} else {
return this.uploadText;
}
},

didReceiveAttrs() {
this._super(...arguments);
this._updateIcon();
},

click() {
if (this.isUploading) {
this.resumable.cancel();
later(() => this._reset());
return false;
} else {
return true;
}
},

_updateIcon() {
const icon = this.isUploading ? "times" : "upload";
this.set("uploadingIcon", `${iconHTML(icon)}`.htmlSafe());
},

_updateProgressBar() {
const pb = `${"width:" + this.progress + "%"}`.htmlSafe();
this.set("progressBar", pb);
},

_reset() {
this.setProperties({ isUploading: false, progress: 0 });
this._updateIcon();
this._updateProgressBar();
},
});
@@ -0,0 +1,92 @@
import discourseComputed, { on } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import ScreenedIpAddress from "admin/models/screened-ip-address";
import bootbox from "bootbox";
import { schedule } from "@ember/runloop";

/**
A form to create an IP address that will be blocked or allowed.
Example usage:
{{screened-ip-address-form action=(action "recordAdded")}}
where action is a callback on the controller or route that will get called after
the new record is successfully saved. It is called with the new ScreenedIpAddress record
as an argument.
**/

export default Component.extend({
classNames: ["screened-ip-address-form"],
formSubmitted: false,
actionName: "block",

@discourseComputed("siteSettings.use_admin_ip_allowlist")
actionNames(adminAllowlistEnabled) {
if (adminAllowlistEnabled) {
return [
{ id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") },
{
id: "do_nothing",
name: I18n.t("admin.logs.screened_ips.actions.do_nothing"),
},
{
id: "allow_admin",
name: I18n.t("admin.logs.screened_ips.actions.allow_admin"),
},
];
} else {
return [
{ id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") },
{
id: "do_nothing",
name: I18n.t("admin.logs.screened_ips.actions.do_nothing"),
},
];
}
},

actions: {
submit() {
if (!this.formSubmitted) {
this.set("formSubmitted", true);
const screenedIpAddress = ScreenedIpAddress.create({
ip_address: this.ip_address,
action_name: this.actionName,
});
screenedIpAddress
.save()
.then((result) => {
this.setProperties({ ip_address: "", formSubmitted: false });
this.action(ScreenedIpAddress.create(result.screened_ip_address));
schedule("afterRender", () =>
this.element.querySelector(".ip-address-input").focus()
);
})
.catch((e) => {
this.set("formSubmitted", false);
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg, () =>
this.element.querySelector(".ip-address-input").focus()
);
});
}
},
},

@on("didInsertElement")
_init() {
schedule("afterRender", () => {
$(this.element.querySelector(".ip-address-input")).keydown((e) => {
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},
});
@@ -0,0 +1,111 @@
import Component from "@ember/component";
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { on } from "discourse-common/utils/decorators";
import { set } from "@ember/object";

export default Component.extend({
classNameBindings: [":value-list", ":secret-value-list"],
inputDelimiter: null,
collection: null,
values: null,
validationMessage: null,

@on("didReceiveAttrs")
_setupCollection() {
const values = this.values;

this.set(
"collection",
this._splitValues(values, this.inputDelimiter || "\n")
);
},

actions: {
changeKey(index, newValue) {
if (this._checkInvalidInput(newValue)) {
return;
}
this._replaceValue(index, newValue, "key");
},

changeSecret(index, newValue) {
if (this._checkInvalidInput(newValue)) {
return;
}
this._replaceValue(index, newValue, "secret");
},

addValue() {
if (this._checkInvalidInput([this.newKey, this.newSecret])) {
return;
}
this._addValue(this.newKey, this.newSecret);
this.setProperties({ newKey: "", newSecret: "" });
},

removeValue(value) {
this._removeValue(value);
},
},

_checkInvalidInput(inputs) {
this.set("validationMessage", null);
for (let input of inputs) {
if (isEmpty(input) || input.includes("|")) {
this.set(
"validationMessage",
I18n.t("admin.site_settings.secret_list.invalid_input")
);
return true;
}
}
},

_addValue(value, secret) {
this.collection.addObject({ key: value, secret: secret });
this._saveValues();
},

_removeValue(value) {
const collection = this.collection;
collection.removeObject(value);
this._saveValues();
},

_replaceValue(index, newValue, keyName) {
let item = this.collection[index];
set(item, keyName, newValue);

this._saveValues();
},

_saveValues() {
this.set(
"values",
this.collection
.map(function (elem) {
return `${elem.key}|${elem.secret}`;
})
.join("\n")
);
},

_splitValues(values, delimiter) {
if (values && values.length) {
const keys = ["key", "secret"];
let res = [];
values.split(delimiter).forEach(function (str) {
let object = {};
str.split("|").forEach(function (a, i) {
object[keys[i]] = a;
});
res.push(object);
});

return res;
} else {
return [];
}
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});
@@ -0,0 +1,59 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { empty } from "@ember/object/computed";
import { on } from "discourse-common/utils/decorators";

export default Component.extend({
classNameBindings: [":simple-list", ":value-list"],
inputEmpty: empty("newValue"),
inputDelimiter: null,
newValue: "",
collection: null,
values: null,

@on("didReceiveAttrs")
_setupCollection() {
this.set("collection", this._splitValues(this.values, this.inputDelimiter));
},

keyDown(event) {
if (event.which === 13) {
this.addValue(this.newValue);
return;
}
},

@action
changeValue(index, newValue) {
this.collection.replace(index, 1, [newValue]);
this.collection.arrayContentDidChange(index);
this._onChange();
},

@action
addValue(newValue) {
if (this.inputEmpty) {
return;
}

this.set("newValue", null);
this.collection.addObject(newValue);
this._onChange();
},

@action
removeValue(value) {
this.collection.removeObject(value);
this._onChange();
},

_onChange() {
this.attrs.onChange && this.attrs.onChange(this.collection);
},

_splitValues(values, delimiter) {
return values && values.length
? values.split(delimiter || "\n").filter(Boolean)
: [];
},
});
@@ -0,0 +1,18 @@
import BufferedContent from "discourse/mixins/buffered-content";
import Component from "@ember/component";
import SettingComponent from "admin/mixins/setting-component";
import SiteSetting from "admin/models/site-setting";
import { readOnly } from "@ember/object/computed";

export default Component.extend(BufferedContent, SettingComponent, {
updateExistingUsers: null,

_save() {
const setting = this.buffered;
return SiteSetting.update(setting.get("setting"), setting.get("value"), {
updateExistingUsers: this.updateExistingUsers,
});
},

staffLogFilter: readOnly("setting.staffLogFilter"),
});
@@ -0,0 +1,6 @@
import ImageUploader from "discourse/components/image-uploader";

export default ImageUploader.extend({
layoutName: "components/image-uploader",
uploadUrlParams: "&for_site_setting=true",
});
@@ -0,0 +1,19 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";

export default Component.extend({
@discourseComputed("value")
enabled: {
get(value) {
if (isEmpty(value)) {
return false;
}
return value.toString() === "true";
},
set(value) {
this.set("value", value ? "true" : "false");
return value;
},
},
});
@@ -0,0 +1,15 @@
import Category from "discourse/models/category";
import Component from "@ember/component";
import { computed } from "@ember/object";

export default Component.extend({
selectedCategories: computed("value", function () {
return Category.findByIds(this.value.split("|").filter(Boolean));
}),

actions: {
onChangeSelectedCategories(value) {
this.set("value", (value || []).mapBy("id").join("|"));
},
},
});
@@ -0,0 +1,52 @@
import { action, computed } from "@ember/object";
import Component from "@ember/component";

function RGBToHex(rgb) {
// Choose correct separator
let sep = rgb.indexOf(",") > -1 ? "," : " ";
// Turn "rgb(r,g,b)" into [r,g,b]
rgb = rgb.substr(4).split(")")[0].split(sep);

let r = (+rgb[0]).toString(16),
g = (+rgb[1]).toString(16),
b = (+rgb[2]).toString(16);

if (r.length === 1) {
r = "0" + r;
}
if (g.length === 1) {
g = "0" + g;
}
if (b.length === 1) {
b = "0" + b;
}

return "#" + r + g + b;
}

export default Component.extend({
valid: computed("value", function () {
let value = this.value.toLowerCase();

let testColor = new Option().style;
testColor.color = value;

if (!testColor.color && !value.startsWith("#")) {
value = `#${value}`;
testColor = new Option().style;
testColor.color = value;
}

let hexifiedColor = RGBToHex(testColor.color);
if (hexifiedColor.includes("NaN")) {
hexifiedColor = testColor.color;
}

return testColor.color && hexifiedColor === value;
}),

@action
onChangeColor(color) {
this.set("value", color);
},
});
@@ -0,0 +1,40 @@
import Component from "@ember/component";
import { computed } from "@ember/object";
import { makeArray } from "discourse-common/lib/helpers";

export default Component.extend({
tokenSeparator: "|",

createdChoices: null,

settingValue: computed("value", function () {
return this.value.toString().split(this.tokenSeparator).filter(Boolean);
}),

settingChoices: computed(
"settingValue",
"setting.choices.[]",
"createdChoices.[]",
function () {
return [
...new Set([
...makeArray(this.settingValue),
...makeArray(this.setting.choices),
...makeArray(this.createdChoices),
]),
];
}
),

actions: {
onChangeListSetting(value) {
this.set("value", value.join(this.tokenSeparator));
},

onChangeChoices(choices) {
this.set("createdChoices", [
...new Set([...makeArray(this.createdChoices), ...makeArray(choices)]),
]);
},
},
});
@@ -0,0 +1,25 @@
import Component from "@ember/component";
import { computed } from "@ember/object";

export default Component.extend({
tokenSeparator: "|",

nameProperty: "name",
valueProperty: "id",

groupChoices: computed("site.groups", function () {
return (this.site.groups || []).map((g) => {
return { name: g.name, id: g.id.toString() };
});
}),

settingValue: computed("value", function () {
return (this.value || "").split(this.tokenSeparator).filter(Boolean);
}),

actions: {
onChangeGroupListSetting(value) {
this.set("value", value.join(this.tokenSeparator));
},
},
});
@@ -0,0 +1,11 @@
import Component from "@ember/component";
import { action } from "@ember/object";

export default Component.extend({
inputDelimiter: "|",

@action
onChange(value) {
this.set("value", value.join(this.inputDelimiter || "\n"));
},
});
@@ -0,0 +1,17 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";

export default Component.extend({
@discourseComputed("value")
selectedTags: {
get(value) {
return value.split("|").filter(Boolean);
},
},

@action
changeSelectedTags(tags) {
this.set("value", tags.join("|"));
},
});
@@ -0,0 +1,16 @@
import Component from "@ember/component";
import showModal from "discourse/lib/show-modal";

export default Component.extend({
actions: {
showUploadModal({ value, setting }) {
showModal("admin-uploaded-image-list", {
admin: true,
title: `admin.site_settings.${setting.setting}.title`,
model: { value, setting },
}).setProperties({
save: (v) => this.set("value", v),
});
},
},
});
@@ -0,0 +1,42 @@
import Component from "@ember/component";
import highlightHTML from "discourse/lib/highlight-html";
import { on } from "discourse-common/utils/decorators";

export default Component.extend({
classNames: ["site-text"],
classNameBindings: ["siteText.overridden"],

@on("didInsertElement")
highlightTerm() {
const term = this._searchTerm();

if (term) {
highlightHTML(
this.element.querySelector(".site-text-id, .site-text-value"),
term,
{
className: "text-highlight",
}
);
}
$(this.element.querySelector(".site-text-value")).ellipsis();
},

click() {
this.editAction(this.siteText);
},

_searchTerm() {
const regex = this.searchRegex;
const siteText = this.siteText;

if (regex && siteText) {
const matches = siteText.value.match(new RegExp(regex, "i"));
if (matches) {
return matches[0];
}
}

return this.term;
},
});
@@ -0,0 +1,39 @@
import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";

export default Component.extend({
classNames: ["table", "staff-actions"],

willDestroyElement() {
$(this.element).off("click.discourse-staff-logs");
},

didInsertElement() {
this._super(...arguments);

$(this.element).on(
"click.discourse-staff-logs",
"[data-link-post-id]",
(e) => {
let postId = $(e.target).attr("data-link-post-id");

this.store.find("post", postId).then((p) => {
DiscourseURL.routeTo(p.get("url"));
});
return false;
}
);

$(this.element).on(
"click.discourse-staff-logs",
"[data-link-topic-id]",
(e) => {
let topicId = $(e.target).attr("data-link-topic-id");

DiscourseURL.routeTo(`/t/${topicId}`);

return false;
}
);
},
});
@@ -0,0 +1,52 @@
import Component from "@ember/component";
import I18n from "I18n";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";

const CUSTOM_REASON_KEY = "custom";

export default Component.extend({
tagName: "",
selectedReason: CUSTOM_REASON_KEY,
customReason: "",
reasonKeys: [
"not_listening_to_staff",
"consuming_staff_time",
"combatative",
"in_wrong_place",
"no_constructive_purpose",
CUSTOM_REASON_KEY,
],
isCustomReason: equal("selectedReason", CUSTOM_REASON_KEY),

@discourseComputed("reasonKeys")
reasons(keys) {
return keys.map((key) => {
return { id: key, name: I18n.t(`admin.user.suspend_reasons.${key}`) };
});
},

@action
setSelectedReason(value) {
this.set("selectedReason", value);
this.setReason();
},

@action
setCustomReason(value) {
this.set("customReason", value);
this.setReason();
},

setReason() {
if (this.isCustomReason) {
this.set("reason", this.customReason);
} else {
this.set(
"reason",
I18n.t(`admin.user.suspend_reasons.${this.selectedReason}`)
);
}
},
});
@@ -0,0 +1,23 @@
import Component from "@ember/component";
import I18n from "I18n";
import UploadMixin from "discourse/mixins/upload";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";

export default Component.extend(UploadMixin, {
type: "csv",
uploadUrl: "/tags/upload",
addDisabled: alias("uploading"),
elementId: "tag-uploader",

validateUploadedFilesOptions() {
return { csvOnly: true };
},

uploadDone() {
bootbox.alert(I18n.t("tagging.upload_successful"), () => {
this.refresh();
this.closeModal();
});
},
});
@@ -0,0 +1,20 @@
import BufferedContent from "discourse/mixins/buffered-content";
import Component from "@ember/component";
import SettingComponent from "admin/mixins/setting-component";
import { ajax } from "discourse/lib/ajax";
import { url } from "discourse/lib/computed";

export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
updateUrl: url("model.id", "/admin/themes/%@/setting"),

_save() {
return ajax(this.updateUrl, {
type: "PUT",
data: {
name: this.setting.setting,
value: this.get("buffered.value"),
},
});
},
});
@@ -0,0 +1,27 @@
import BufferedContent from "discourse/mixins/buffered-content";
import Component from "@ember/component";
import SettingComponent from "admin/mixins/setting-component";

export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",

_save() {
return this.model
.save({ [this.setting.setting]: this.convertNamesToIds() })
.then(() => this.store.findAll("theme"));
},

convertNamesToIds() {
return this.get("buffered.value")
.split("|")
.filter(Boolean)
.map((themeName) => {
if (themeName !== "") {
return this.setting.allThemes.find(
(theme) => theme.name === themeName
).id;
}
return themeName;
});
},
});
@@ -0,0 +1,18 @@
import BufferedContent from "discourse/mixins/buffered-content";
import Component from "@ember/component";
import SettingComponent from "admin/mixins/setting-component";
import { alias } from "@ember/object/computed";

export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
setting: alias("translation"),
type: "string",
settingName: alias("translation.key"),

_save() {
return this.model.saveTranslation(
this.get("translation.key"),
this.get("buffered.value")
);
},
});
@@ -0,0 +1,147 @@
import { and, gt } from "@ember/object/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { escape } from "pretty-text/sanitizer";
import { iconHTML } from "discourse-common/lib/icon-library";
import { isTesting } from "discourse-common/config/environment";
import { schedule } from "@ember/runloop";

const MAX_COMPONENTS = 4;

export default Component.extend({
childrenExpanded: false,
classNames: ["themes-list-item"],
classNameBindings: ["theme.selected:selected"],
hasComponents: gt("children.length", 0),
displayComponents: and("hasComponents", "theme.isActive"),
displayHasMore: gt("theme.childThemes.length", MAX_COMPONENTS),

click(e) {
if (!$(e.target).hasClass("others-count")) {
this.navigateToTheme();
}
},

init() {
this._super(...arguments);
this.scheduleAnimation();
},

@observes("theme.selected")
triggerAnimation() {
this.animate();
},

scheduleAnimation() {
schedule("afterRender", () => {
this.animate(true);
});
},

animate(isInitial) {
const $container = $(this.element);
const $list = $(this.element.querySelector(".components-list"));
if ($list.length === 0 || isTesting()) {
return;
}
const duration = 300;
if (this.get("theme.selected")) {
this.collapseComponentsList($container, $list, duration);
} else if (!isInitial) {
this.expandComponentsList($container, $list, duration);
}
},

@discourseComputed(
"theme.component",
"theme.childThemes.@each.name",
"theme.childThemes.length",
"childrenExpanded"
)
children() {
const theme = this.theme;
let children = theme.get("childThemes");
if (theme.get("component") || !children) {
return [];
}
children = this.childrenExpanded
? children
: children.slice(0, MAX_COMPONENTS);
return children.map((t) => {
const name = escape(t.name);
return t.enabled ? name : `${iconHTML("ban")} ${name}`;
});
},

@discourseComputed("children")
childrenString(children) {
return children.join(", ");
},

@discourseComputed(
"theme.childThemes.length",
"theme.component",
"childrenExpanded",
"children.length"
)
moreCount(childrenCount, component, expanded) {
if (component || !childrenCount || expanded) {
return 0;
}
return childrenCount - MAX_COMPONENTS;
},

expandComponentsList($container, $list, duration) {
$container.css("height", `${$container.height()}px`);
$list.css("display", "");
$container.animate(
{
height: `${$container.height() + $list.outerHeight(true)}px`,
},
{
duration,
done: () => {
$list.css("display", "");
$container.css("height", "");
},
}
);
$list.animate(
{
opacity: 1,
},
{
duration,
}
);
},

collapseComponentsList($container, $list, duration) {
$container.animate(
{
height: `${$container.height() - $list.outerHeight(true)}px`,
},
{
duration,
done: () => {
$list.css("display", "none");
$container.css("height", "");
},
}
);
$list.animate(
{
opacity: 0,
},
{
duration,
}
);
},

actions: {
toggleChildrenExpanded() {
this.toggleProperty("childrenExpanded");
},
},
});
@@ -0,0 +1,81 @@
import { COMPONENTS, THEMES } from "admin/models/theme";
import { equal, gt } from "@ember/object/computed";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";

export default Component.extend({
router: service(),
THEMES,
COMPONENTS,

classNames: ["themes-list"],

hasThemes: gt("themesList.length", 0),
hasActiveThemes: gt("activeThemes.length", 0),
hasInactiveThemes: gt("inactiveThemes.length", 0),

themesTabActive: equal("currentTab", THEMES),
componentsTabActive: equal("currentTab", COMPONENTS),

@discourseComputed("themes", "components", "currentTab")
themesList(themes, components) {
if (this.themesTabActive) {
return themes;
} else {
return components;
}
},

@discourseComputed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
"themesList.@each.default"
)
inactiveThemes(themes) {
if (this.componentsTabActive) {
return themes.filter((theme) => theme.get("parent_themes.length") <= 0);
}
return themes.filter(
(theme) => !theme.get("user_selectable") && !theme.get("default")
);
},

@discourseComputed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
"themesList.@each.default"
)
activeThemes(themes) {
if (this.componentsTabActive) {
return themes.filter((theme) => theme.get("parent_themes.length") > 0);
} else {
return themes
.filter((theme) => theme.get("user_selectable") || theme.get("default"))
.sort((a, b) => {
if (a.get("default") && !b.get("default")) {
return -1;
} else if (b.get("default")) {
return 1;
}
return a
.get("name")
.toLowerCase()
.localeCompare(b.get("name").toLowerCase());
});
}
},

actions: {
changeView(newTab) {
if (newTab !== this.currentTab) {
this.set("currentTab", newTab);
}
},
navigateToTheme(theme) {
this.router.transitionTo("adminCustomizeThemes.show", theme);
},
},
});
@@ -0,0 +1,109 @@
import discourseComputed, { on } from "discourse-common/utils/decorators";
import { empty, reads } from "@ember/object/computed";
import Component from "@ember/component";
import { makeArray } from "discourse-common/lib/helpers";

export default Component.extend({
classNameBindings: [":value-list"],
inputInvalid: empty("newValue"),
inputDelimiter: null,
inputType: null,
newValue: "",
collection: null,
values: null,
noneKey: reads("addKey"),

@on("didReceiveAttrs")
_setupCollection() {
const values = this.values;
if (this.inputType === "array") {
this.set("collection", values || []);
return;
}

this.set(
"collection",
this._splitValues(values, this.inputDelimiter || "\n")
);
},

@discourseComputed("choices.[]", "collection.[]")
filteredChoices(choices, collection) {
return makeArray(choices).filter((i) => collection.indexOf(i) < 0);
},

keyDown(event) {
if (event.keyCode === 13) {
this.send("addValue", this.newValue);
}
},

actions: {
changeValue(index, newValue) {
this._replaceValue(index, newValue);
},

addValue(newValue) {
if (this.inputInvalid) {
return;
}

this.set("newValue", null);
this._addValue(newValue);
},

removeValue(value) {
this._removeValue(value);
},

selectChoice(choice) {
this._addValue(choice);
},
},

_addValue(value) {
this.collection.addObject(value);

if (this.choices) {
this.set("choices", this.choices.rejectBy("id", value));
} else {
this.set("choices", []);
}

this._saveValues();
},

_removeValue(value) {
this.collection.removeObject(value);

if (this.choices) {
this.set("choices", this.choices.concat([value]).uniq());
} else {
this.set("choices", makeArray(value));
}

this._saveValues();
},

_replaceValue(index, newValue) {
this.collection.replace(index, 1, [newValue]);
this._saveValues();
},

_saveValues() {
if (this.inputType === "array") {
this.set("values", this.collection);
return;
}

this.set("values", this.collection.join(this.inputDelimiter || "\n"));
},

_splitValues(values, delimiter) {
if (values && values.length) {
return values.split(delimiter).filter((x) => x);
} else {
return [];
}
},
});
@@ -0,0 +1,102 @@
import discourseComputed, {
observes,
on,
} from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import WatchedWord from "admin/models/watched-word";
import bootbox from "bootbox";
import { isEmpty } from "@ember/utils";
import { schedule } from "@ember/runloop";

export default Component.extend({
classNames: ["watched-word-form"],
formSubmitted: false,
actionKey: null,
showMessage: false,

@discourseComputed("regularExpressions")
placeholderKey(regularExpressions) {
return (
"admin.watched_words.form.placeholder" +
(regularExpressions ? "_regexp" : "")
);
},

@observes("word")
removeMessage() {
if (this.showMessage && !isEmpty(this.word)) {
this.set("showMessage", false);
}
},

@discourseComputed("word")
isUniqueWord(word) {
const words = this.filteredContent || [];
const filtered = words.filter(
(content) => content.action === this.actionKey
);
return filtered.every(
(content) => content.word.toLowerCase() !== word.toLowerCase()
);
},

actions: {
submit() {
if (!this.isUniqueWord) {
this.setProperties({
showMessage: true,
message: I18n.t("admin.watched_words.form.exists"),
});
return;
}

if (!this.formSubmitted) {
this.set("formSubmitted", true);

const watchedWord = WatchedWord.create({
word: this.word,
action: this.actionKey,
});

watchedWord
.save()
.then((result) => {
this.setProperties({
word: "",
formSubmitted: false,
showMessage: true,
message: I18n.t("admin.watched_words.form.success"),
});
this.action(WatchedWord.create(result));
schedule("afterRender", () =>
this.element.querySelector(".watched-word-input").focus()
);
})
.catch((e) => {
this.set("formSubmitted", false);
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg, () =>
this.element.querySelector(".watched-word-input").focus()
);
});
}
},
},

@on("didInsertElement")
_init() {
schedule("afterRender", () => {
$(this.element.querySelector(".watched-word-input")).keydown((e) => {
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},
});
@@ -0,0 +1,29 @@
import Component from "@ember/component";
import I18n from "I18n";
import UploadMixin from "discourse/mixins/upload";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";

export default Component.extend(UploadMixin, {
type: "txt",
classNames: "watched-words-uploader",
uploadUrl: "/admin/logs/watched_words/upload",
addDisabled: alias("uploading"),

validateUploadedFilesOptions() {
return { skipValidation: true };
},

@discourseComputed("actionKey")
data(actionKey) {
return { action_key: actionKey };
},

uploadDone() {
if (this) {
bootbox.alert(I18n.t("admin.watched_words.form.upload_successful"));
this.done();
}
},
});
@@ -0,0 +1,14 @@
import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";

export default Controller.extend({
actions: {
revokeKey(key) {
key.revoke().catch(popupAjaxError);
},

undoRevokeKey(key) {
key.undoRevoke().catch(popupAjaxError);
},
},
});
@@ -0,0 +1,72 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isBlank } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { get } from "@ember/object";
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: {
updateUsername(selected) {
this.set("model.username", get(selected, "firstObject"));
},

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 Controller from "@ember/controller";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { empty } from "@ember/object/computed";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
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,
},
});
},
},
});
File renamed without changes.
@@ -0,0 +1,60 @@
import Controller, { inject as controller } from "@ember/controller";
import { alias, equal } from "@ember/object/computed";
import { i18n, setting } from "discourse/lib/computed";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";

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 Controller, { inject as controller } from "@ember/controller";
import { alias } from "@ember/object/computed";

export default Controller.extend({
adminBackups: controller(),
status: alias("adminBackups.model"),

init() {
this._super(...arguments);

this.logs = [];
},
});
@@ -0,0 +1,11 @@
import { and, not } 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 Controller from "@ember/controller";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { popupAjaxError } from "discourse/lib/ajax-error";

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,173 @@
import Controller, { inject as controller } from "@ember/controller";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import I18n from "I18n";
import bootbox from "bootbox";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyNotEqual } from "discourse/lib/computed";
import { reads } from "@ember/object/computed";
import { run } from "@ember/runloop";

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

@discourseComputed("model.i18n_name")
textCustomizationPrefix(i18n_name) {
return `badges.${i18n_name}.`;
},

@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) {
let 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 discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";

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 Controller from "@ember/controller";
import I18n from "I18n";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { later } from "@ember/runloop";

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 Controller from "@ember/controller";
import EmberObject from "@ember/object";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import showModal from "discourse/lib/show-modal";

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 Controller from "@ember/controller";
import I18n from "I18n";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";

export default Controller.extend({
@discourseComputed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
},

@discourseComputed("model.changed", "model.isSaving")
saveDisabled(changed, isSaving) {
return !changed || isSaving;
},

actions: {
save() {
if (!this.model.saving) {
this.set("saving", true);
this.model
.update(this.model.getProperties("html", "css"))
.catch((e) => {
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("admin.customize.email_style.save_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg);
})
.finally(() => this.set("model.changed", false));
}
},
},
});
@@ -0,0 +1,61 @@
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import { action } from "@ember/object";
import bootbox from "bootbox";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";

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 Controller from "@ember/controller";
import { action } from "@ember/object";
import { sort } from "@ember/object/computed";

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 Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { not } from "@ember/object/computed";
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,72 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { url } from "discourse/lib/computed";

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

goBack() {
this.replaceRoute(this.showRouteName, this.model.id);
},
},
});