REFACTOR: Support bundling our admin section as an ember addon
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const GENERAL_ATTRIBUTES = [
|
||||
"updated_at",
|
||||
"discourse_updated_at",
|
||||
"release_notes_link",
|
||||
];
|
||||
|
||||
const AdminDashboard = EmberObject.extend({});
|
||||
|
||||
AdminDashboard.reopenClass({
|
||||
fetch() {
|
||||
return ajax("/admin/dashboard.json").then((json) => {
|
||||
const model = AdminDashboard.create();
|
||||
model.set("version_check", json.version_check);
|
||||
return model;
|
||||
});
|
||||
},
|
||||
|
||||
fetchGeneral() {
|
||||
return ajax("/admin/dashboard/general.json").then((json) => {
|
||||
const model = AdminDashboard.create();
|
||||
|
||||
const attributes = {};
|
||||
GENERAL_ATTRIBUTES.forEach((a) => (attributes[a] = json[a]));
|
||||
|
||||
model.setProperties({
|
||||
reports: json.reports,
|
||||
attributes,
|
||||
loaded: true,
|
||||
});
|
||||
|
||||
return model;
|
||||
});
|
||||
},
|
||||
|
||||
fetchProblems() {
|
||||
return ajax("/admin/dashboard/problems.json").then((json) => {
|
||||
const model = AdminDashboard.create(json);
|
||||
model.set("loaded", true);
|
||||
return model;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default AdminDashboard;
|
||||
@@ -0,0 +1,597 @@
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { filter, or, gt, lt, not } from "@ember/object/computed";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { propertyNotEqual } from "discourse/lib/computed";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import Group from "discourse/models/group";
|
||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import { Promise } from "rsvp";
|
||||
import User from "discourse/models/user";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
const wrapAdmin = (user) => (user ? AdminUser.create(user) : null);
|
||||
|
||||
const AdminUser = User.extend({
|
||||
adminUserView: true,
|
||||
customGroups: filter("groups", (g) => !g.automatic && Group.create(g)),
|
||||
automaticGroups: filter("groups", (g) => g.automatic && Group.create(g)),
|
||||
|
||||
canViewProfile: or("active", "staged"),
|
||||
|
||||
@discourseComputed("bounce_score", "reset_bounce_score_after")
|
||||
bounceScore(bounce_score, reset_bounce_score_after) {
|
||||
if (bounce_score > 0) {
|
||||
return `${bounce_score} - ${moment(reset_bounce_score_after).format(
|
||||
"LL"
|
||||
)}`;
|
||||
} else {
|
||||
return bounce_score;
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("bounce_score")
|
||||
bounceScoreExplanation(bounce_score) {
|
||||
if (bounce_score === 0) {
|
||||
return I18n.t("admin.user.bounce_score_explanation.none");
|
||||
} else if (bounce_score < this.siteSettings.bounce_score_threshold) {
|
||||
return I18n.t("admin.user.bounce_score_explanation.some");
|
||||
} else {
|
||||
return I18n.t("admin.user.bounce_score_explanation.threshold_reached");
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
bounceLink() {
|
||||
return getURL("/admin/email/bounced");
|
||||
},
|
||||
|
||||
canResetBounceScore: gt("bounce_score", 0),
|
||||
|
||||
resetBounceScore() {
|
||||
return ajax(`/admin/users/${this.id}/reset_bounce_score`, {
|
||||
type: "POST",
|
||||
}).then(() =>
|
||||
this.setProperties({
|
||||
bounce_score: 0,
|
||||
reset_bounce_score_after: null,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
groupAdded(added) {
|
||||
return ajax(`/admin/users/${this.id}/groups`, {
|
||||
type: "POST",
|
||||
data: { group_id: added.id },
|
||||
}).then(() => this.groups.pushObject(added));
|
||||
},
|
||||
|
||||
groupRemoved(groupId) {
|
||||
return ajax(`/admin/users/${this.id}/groups/${groupId}`, {
|
||||
type: "DELETE",
|
||||
}).then(() => {
|
||||
this.set("groups.[]", this.groups.rejectBy("id", groupId));
|
||||
if (this.primary_group_id === groupId) {
|
||||
this.set("primary_group_id", null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteAllPosts() {
|
||||
let deletedPosts = 0;
|
||||
const user = this;
|
||||
const message = I18n.messageFormat(
|
||||
"admin.user.delete_all_posts_confirm_MF",
|
||||
{
|
||||
POSTS: user.get("post_count"),
|
||||
TOPICS: user.get("topic_count"),
|
||||
}
|
||||
);
|
||||
const buttons = [
|
||||
{
|
||||
label: I18n.t("composer.cancel"),
|
||||
class: "d-modal-cancel",
|
||||
link: true,
|
||||
},
|
||||
{
|
||||
label:
|
||||
`${iconHTML("exclamation-triangle")} ` +
|
||||
I18n.t("admin.user.delete_all_posts"),
|
||||
class: "btn btn-danger",
|
||||
callback: () => {
|
||||
openProgressModal();
|
||||
performDelete();
|
||||
},
|
||||
},
|
||||
];
|
||||
const openProgressModal = () => {
|
||||
bootbox.dialog(
|
||||
`<p>${I18n.t(
|
||||
"admin.user.delete_posts_progress"
|
||||
)}</p><div class='progress-bar'><span></span></div>`,
|
||||
[],
|
||||
{ classes: "delete-posts-progress" }
|
||||
);
|
||||
};
|
||||
const performDelete = () => {
|
||||
let deletedPercentage = 0;
|
||||
return ajax(`/admin/users/${user.get("id")}/delete_posts_batch`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then(({ posts_deleted }) => {
|
||||
if (posts_deleted === 0) {
|
||||
user.set("post_count", 0);
|
||||
bootbox.hideAll();
|
||||
} else {
|
||||
deletedPosts += posts_deleted;
|
||||
deletedPercentage = Math.floor(
|
||||
(deletedPosts * 100) / user.get("post_count")
|
||||
);
|
||||
$(".delete-posts-progress .progress-bar > span").css({
|
||||
width: `${deletedPercentage}%`,
|
||||
});
|
||||
performDelete();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
bootbox.hideAll();
|
||||
let error;
|
||||
AdminUser.find(user.get("id")).then((u) => user.setProperties(u));
|
||||
if (e.responseJSON && e.responseJSON.errors) {
|
||||
error = e.responseJSON.errors[0];
|
||||
}
|
||||
error = error || I18n.t("admin.user.delete_posts_failed");
|
||||
bootbox.alert(error);
|
||||
});
|
||||
};
|
||||
|
||||
bootbox.dialog(message, buttons, { classes: "delete-all-posts" });
|
||||
},
|
||||
|
||||
revokeAdmin() {
|
||||
return ajax(`/admin/users/${this.id}/revoke_admin`, {
|
||||
type: "PUT",
|
||||
}).then(() => {
|
||||
this.setProperties({
|
||||
admin: false,
|
||||
can_grant_admin: true,
|
||||
can_revoke_admin: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
grantAdmin() {
|
||||
return ajax(`/admin/users/${this.id}/grant_admin`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then(() => {
|
||||
bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
revokeModeration() {
|
||||
return ajax(`/admin/users/${this.id}/revoke_moderation`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then(() => {
|
||||
this.setProperties({
|
||||
moderator: false,
|
||||
can_grant_moderation: true,
|
||||
can_revoke_moderation: false,
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
grantModeration() {
|
||||
return ajax(`/admin/users/${this.id}/grant_moderation`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then(() => {
|
||||
this.setProperties({
|
||||
moderator: true,
|
||||
can_grant_moderation: false,
|
||||
can_revoke_moderation: true,
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
disableSecondFactor() {
|
||||
return ajax(`/admin/users/${this.id}/disable_second_factor`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then(() => {
|
||||
this.set("second_factor_enabled", false);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
approve(approvedBy) {
|
||||
return ajax(`/admin/users/${this.id}/approve`, {
|
||||
type: "PUT",
|
||||
}).then(() => {
|
||||
this.setProperties({
|
||||
can_approve: false,
|
||||
approved: true,
|
||||
approved_by: approvedBy,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setOriginalTrustLevel() {
|
||||
this.set("originalTrustLevel", this.trust_level);
|
||||
},
|
||||
|
||||
dirty: propertyNotEqual("originalTrustLevel", "trust_level"),
|
||||
|
||||
saveTrustLevel() {
|
||||
return ajax(`/admin/users/${this.id}/trust_level`, {
|
||||
type: "PUT",
|
||||
data: { level: this.trust_level },
|
||||
})
|
||||
.then(() => window.location.reload())
|
||||
.catch((e) => {
|
||||
let error;
|
||||
if (e.responseJSON && e.responseJSON.errors) {
|
||||
error = e.responseJSON.errors[0];
|
||||
}
|
||||
error =
|
||||
error ||
|
||||
I18n.t("admin.user.trust_level_change_failed", {
|
||||
error: this._formatError(e),
|
||||
});
|
||||
bootbox.alert(error);
|
||||
});
|
||||
},
|
||||
|
||||
restoreTrustLevel() {
|
||||
this.set("trust_level", this.originalTrustLevel);
|
||||
},
|
||||
|
||||
lockTrustLevel(locked) {
|
||||
return ajax(`/admin/users/${this.id}/trust_level_lock`, {
|
||||
type: "PUT",
|
||||
data: { locked: !!locked },
|
||||
})
|
||||
.then(() => window.location.reload())
|
||||
.catch((e) => {
|
||||
let error;
|
||||
if (e.responseJSON && e.responseJSON.errors) {
|
||||
error = e.responseJSON.errors[0];
|
||||
}
|
||||
error =
|
||||
error ||
|
||||
I18n.t("admin.user.trust_level_change_failed", {
|
||||
error: this._formatError(e),
|
||||
});
|
||||
bootbox.alert(error);
|
||||
});
|
||||
},
|
||||
|
||||
canLockTrustLevel: lt("trust_level", 4),
|
||||
|
||||
canSuspend: not("staff"),
|
||||
|
||||
@discourseComputed("suspended_till", "suspended_at")
|
||||
suspendDuration(suspendedTill, suspendedAt) {
|
||||
suspendedAt = moment(suspendedAt);
|
||||
suspendedTill = moment(suspendedTill);
|
||||
return suspendedAt.format("L") + " - " + suspendedTill.format("L");
|
||||
},
|
||||
|
||||
suspend(data) {
|
||||
return ajax(`/admin/users/${this.id}/suspend`, {
|
||||
type: "PUT",
|
||||
data,
|
||||
}).then((result) => this.setProperties(result.suspension));
|
||||
},
|
||||
|
||||
unsuspend() {
|
||||
return ajax(`/admin/users/${this.id}/unsuspend`, {
|
||||
type: "PUT",
|
||||
}).then((result) => this.setProperties(result.suspension));
|
||||
},
|
||||
|
||||
logOut() {
|
||||
return ajax("/admin/users/" + this.id + "/log_out", {
|
||||
type: "POST",
|
||||
data: { username_or_email: this.username },
|
||||
}).then(() => bootbox.alert(I18n.t("admin.user.logged_out")));
|
||||
},
|
||||
|
||||
impersonate() {
|
||||
return ajax("/admin/impersonate", {
|
||||
type: "POST",
|
||||
data: { username_or_email: this.username },
|
||||
})
|
||||
.then(() => (document.location = getURL("/")))
|
||||
.catch((e) => {
|
||||
if (e.status === 404) {
|
||||
bootbox.alert(I18n.t("admin.impersonate.not_found"));
|
||||
} else {
|
||||
bootbox.alert(I18n.t("admin.impersonate.invalid"));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
activate() {
|
||||
return ajax(`/admin/users/${this.id}/activate`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then(() => window.location.reload())
|
||||
.catch((e) => {
|
||||
const error = I18n.t("admin.user.activate_failed", {
|
||||
error: this._formatError(e),
|
||||
});
|
||||
bootbox.alert(error);
|
||||
});
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
return ajax(`/admin/users/${this.id}/deactivate`, {
|
||||
type: "PUT",
|
||||
data: { context: document.location.pathname },
|
||||
})
|
||||
.then(() => window.location.reload())
|
||||
.catch((e) => {
|
||||
const error = I18n.t("admin.user.deactivate_failed", {
|
||||
error: this._formatError(e),
|
||||
});
|
||||
bootbox.alert(error);
|
||||
});
|
||||
},
|
||||
|
||||
unsilence() {
|
||||
this.set("silencingUser", true);
|
||||
|
||||
return ajax(`/admin/users/${this.id}/unsilence`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then((result) => this.setProperties(result.unsilence))
|
||||
.catch((e) => {
|
||||
const error = I18n.t("admin.user.unsilence_failed", {
|
||||
error: this._formatError(e),
|
||||
});
|
||||
bootbox.alert(error);
|
||||
})
|
||||
.finally(() => this.set("silencingUser", false));
|
||||
},
|
||||
|
||||
silence(data) {
|
||||
this.set("silencingUser", true);
|
||||
return ajax(`/admin/users/${this.id}/silence`, {
|
||||
type: "PUT",
|
||||
data,
|
||||
})
|
||||
.then((result) => this.setProperties(result.silence))
|
||||
.catch((e) => {
|
||||
const error = I18n.t("admin.user.silence_failed", {
|
||||
error: this._formatError(e),
|
||||
});
|
||||
bootbox.alert(error);
|
||||
})
|
||||
.finally(() => this.set("silencingUser", false));
|
||||
},
|
||||
|
||||
sendActivationEmail() {
|
||||
return ajax(userPath("action/send_activation_email"), {
|
||||
type: "POST",
|
||||
data: { username: this.username },
|
||||
})
|
||||
.then(() => bootbox.alert(I18n.t("admin.user.activation_email_sent")))
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
anonymize() {
|
||||
const user = this;
|
||||
const message = I18n.t("admin.user.anonymize_confirm");
|
||||
|
||||
const performAnonymize = function () {
|
||||
return ajax(`/admin/users/${user.get("id")}/anonymize.json`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then(function (data) {
|
||||
if (data.success) {
|
||||
if (data.username) {
|
||||
document.location = getURL(
|
||||
`/admin/users/${user.get("id")}/${data.username}`
|
||||
);
|
||||
} else {
|
||||
document.location = getURL("/admin/users/list/active");
|
||||
}
|
||||
} else {
|
||||
bootbox.alert(I18n.t("admin.user.anonymize_failed"));
|
||||
if (data.user) {
|
||||
user.setProperties(data.user);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => bootbox.alert(I18n.t("admin.user.anonymize_failed")));
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: I18n.t("composer.cancel"),
|
||||
class: "cancel",
|
||||
link: true,
|
||||
},
|
||||
{
|
||||
label:
|
||||
`${iconHTML("exclamation-triangle")} ` +
|
||||
I18n.t("admin.user.anonymize_yes"),
|
||||
class: "btn btn-danger",
|
||||
callback: function () {
|
||||
performAnonymize();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
|
||||
},
|
||||
|
||||
destroy(opts) {
|
||||
const user = this;
|
||||
const message = I18n.t("admin.user.delete_confirm");
|
||||
const location = document.location.pathname;
|
||||
|
||||
const performDestroy = function (block) {
|
||||
bootbox.dialog(I18n.t("admin.user.deleting_user"));
|
||||
let formData = { context: location };
|
||||
if (block) {
|
||||
formData["block_email"] = true;
|
||||
formData["block_urls"] = true;
|
||||
formData["block_ip"] = true;
|
||||
}
|
||||
if (opts && opts.deletePosts) {
|
||||
formData["delete_posts"] = true;
|
||||
}
|
||||
return ajax(`/admin/users/${user.get("id")}.json`, {
|
||||
type: "DELETE",
|
||||
data: formData,
|
||||
})
|
||||
.then(function (data) {
|
||||
if (data.deleted) {
|
||||
if (/^\/admin\/users\/list\//.test(location)) {
|
||||
document.location = location;
|
||||
} else {
|
||||
document.location = getURL("/admin/users/list/active");
|
||||
}
|
||||
} else {
|
||||
bootbox.alert(I18n.t("admin.user.delete_failed"));
|
||||
if (data.user) {
|
||||
user.setProperties(data.user);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
AdminUser.find(user.get("id")).then((u) => user.setProperties(u));
|
||||
bootbox.alert(I18n.t("admin.user.delete_failed"));
|
||||
});
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: I18n.t("composer.cancel"),
|
||||
class: "btn",
|
||||
link: true,
|
||||
},
|
||||
{
|
||||
label:
|
||||
`${iconHTML("exclamation-triangle")} ` +
|
||||
I18n.t("admin.user.delete_and_block"),
|
||||
class: "btn btn-danger",
|
||||
callback: function () {
|
||||
performDestroy(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: I18n.t("admin.user.delete_dont_block"),
|
||||
class: "btn btn-primary",
|
||||
callback: function () {
|
||||
performDestroy(false);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
|
||||
},
|
||||
|
||||
merge(opts) {
|
||||
const user = this;
|
||||
const location = document.location.pathname;
|
||||
|
||||
bootbox.dialog(I18n.t("admin.user.merging_user"));
|
||||
let formData = { context: location };
|
||||
|
||||
if (opts && opts.targetUsername) {
|
||||
formData["target_username"] = opts.targetUsername;
|
||||
}
|
||||
|
||||
return ajax(`/admin/users/${user.id}/merge.json`, {
|
||||
type: "POST",
|
||||
data: formData,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.merged) {
|
||||
if (/^\/admin\/users\/list\//.test(location)) {
|
||||
DiscourseURL.redirectTo(location);
|
||||
} else {
|
||||
DiscourseURL.redirectTo(
|
||||
`/admin/users/${data.user.id}/${data.user.username}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
bootbox.alert(I18n.t("admin.user.merge_failed"));
|
||||
if (data.user) {
|
||||
user.setProperties(data.user);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
AdminUser.find(user.id).then((u) => user.setProperties(u));
|
||||
bootbox.alert(I18n.t("admin.user.merge_failed"));
|
||||
});
|
||||
},
|
||||
|
||||
loadDetails() {
|
||||
if (this.loadedDetails) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
return AdminUser.find(this.id).then((result) => {
|
||||
const userProperties = Object.assign(result, { loadedDetails: true });
|
||||
this.setProperties(userProperties);
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("tl3_requirements")
|
||||
tl3Requirements(requirements) {
|
||||
if (requirements) {
|
||||
return this.store.createRecord("tl3Requirements", requirements);
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("suspended_by")
|
||||
suspendedBy: wrapAdmin,
|
||||
|
||||
@discourseComputed("silenced_by")
|
||||
silencedBy: wrapAdmin,
|
||||
|
||||
@discourseComputed("approved_by")
|
||||
approvedBy: wrapAdmin,
|
||||
|
||||
_formatError(event) {
|
||||
return `http: ${event.status} - ${event.body}`;
|
||||
},
|
||||
|
||||
deleteSSORecord() {
|
||||
return ajax(`/admin/users/${this.id}/sso_record.json`, {
|
||||
type: "DELETE",
|
||||
})
|
||||
.then(() => {
|
||||
this.set("single_sign_on_record", null);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
});
|
||||
|
||||
AdminUser.reopenClass({
|
||||
find(user_id) {
|
||||
return ajax(`/admin/users/${user_id}.json`).then((result) => {
|
||||
result.loadedDetails = true;
|
||||
return AdminUser.create(result);
|
||||
});
|
||||
},
|
||||
|
||||
findAll(query, userFilter) {
|
||||
return ajax(`/admin/users/list/${query}.json`, {
|
||||
data: userFilter,
|
||||
}).then((users) => users.map((u) => AdminUser.create(u)));
|
||||
},
|
||||
});
|
||||
|
||||
export default AdminUser;
|
||||
@@ -0,0 +1,57 @@
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import AdminUser from "admin/models/admin-user";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { computed } from "@ember/object";
|
||||
import { fmt } from "discourse/lib/computed";
|
||||
|
||||
const ApiKey = RestModel.extend({
|
||||
user: computed("_user", {
|
||||
get() {
|
||||
return this._user;
|
||||
},
|
||||
set(key, value) {
|
||||
if (value && !(value instanceof AdminUser)) {
|
||||
this.set("_user", AdminUser.create(value));
|
||||
} else {
|
||||
this.set("_user", value);
|
||||
}
|
||||
return this._user;
|
||||
},
|
||||
}),
|
||||
|
||||
@discourseComputed("description")
|
||||
shortDescription(description) {
|
||||
if (!description || description.length < 40) {
|
||||
return description;
|
||||
}
|
||||
return `${description.substring(0, 40)}...`;
|
||||
},
|
||||
|
||||
truncatedKey: fmt("truncated_key", "%@..."),
|
||||
|
||||
revoke() {
|
||||
return ajax(`${this.basePath}/revoke`, {
|
||||
type: "POST",
|
||||
}).then((result) => this.setProperties(result.api_key));
|
||||
},
|
||||
|
||||
undoRevoke() {
|
||||
return ajax(`${this.basePath}/undo-revoke`, {
|
||||
type: "POST",
|
||||
}).then((result) => this.setProperties(result.api_key));
|
||||
},
|
||||
|
||||
createProperties() {
|
||||
return this.getProperties("description", "username", "scopes");
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
basePath() {
|
||||
return this.store
|
||||
.adapterFor("api-key")
|
||||
.pathFor(this.store, "api-key", this.id);
|
||||
},
|
||||
});
|
||||
|
||||
export default ApiKey;
|
||||
@@ -0,0 +1,12 @@
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { not } from "@ember/object/computed";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
export default EmberObject.extend({
|
||||
restoreDisabled: not("restoreEnabled"),
|
||||
|
||||
@discourseComputed("allowRestore", "isOperationRunning")
|
||||
restoreEnabled(allowRestore, isOperationRunning) {
|
||||
return allowRestore && !isOperationRunning;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "I18n";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import EmberObject from "@ember/object";
|
||||
import MessageBus from "message-bus-client";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
const Backup = EmberObject.extend({
|
||||
destroy() {
|
||||
return ajax("/admin/backups/" + this.filename, { type: "DELETE" });
|
||||
},
|
||||
|
||||
restore() {
|
||||
return ajax("/admin/backups/" + this.filename + "/restore", {
|
||||
type: "POST",
|
||||
data: { client_id: MessageBus.clientId },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Backup.reopenClass({
|
||||
find() {
|
||||
return ajax("/admin/backups.json")
|
||||
.then((backups) => backups.map((backup) => Backup.create(backup)))
|
||||
.catch((error) => {
|
||||
bootbox.alert(
|
||||
I18n.t("admin.backups.backup_storage_error", {
|
||||
error_message: extractError(error),
|
||||
})
|
||||
);
|
||||
return [];
|
||||
});
|
||||
},
|
||||
|
||||
start(withUploads) {
|
||||
if (withUploads === undefined) {
|
||||
withUploads = true;
|
||||
}
|
||||
return ajax("/admin/backups", {
|
||||
type: "POST",
|
||||
data: {
|
||||
with_uploads: withUploads,
|
||||
client_id: MessageBus.clientId,
|
||||
},
|
||||
}).then((result) => {
|
||||
if (!result.success) {
|
||||
bootbox.alert(result.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
cancel() {
|
||||
return ajax("/admin/backups/cancel.json", {
|
||||
type: "DELETE",
|
||||
}).then((result) => {
|
||||
if (!result.success) {
|
||||
bootbox.alert(result.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
rollback() {
|
||||
return ajax("/admin/backups/rollback.json", {
|
||||
type: "POST",
|
||||
}).then((result) => {
|
||||
if (!result.success) {
|
||||
bootbox.alert(result.message);
|
||||
} else {
|
||||
// redirect to homepage (session might be lost)
|
||||
window.location = getURL("/");
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default Backup;
|
||||
@@ -0,0 +1,108 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed, {
|
||||
observes,
|
||||
on,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import { propertyNotEqual } from "discourse/lib/computed";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const ColorSchemeColor = EmberObject.extend({
|
||||
@on("init")
|
||||
startTrackingChanges() {
|
||||
this.set("originals", { hex: this.hex || "FFFFFF" });
|
||||
|
||||
// force changed property to be recalculated
|
||||
this.notifyPropertyChange("hex");
|
||||
},
|
||||
|
||||
// Whether value has changed since it was last saved.
|
||||
@discourseComputed("hex")
|
||||
changed(hex) {
|
||||
if (!this.originals) {
|
||||
return false;
|
||||
}
|
||||
if (hex !== this.originals.hex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// Whether the current value is different than Discourse's default color scheme.
|
||||
overridden: propertyNotEqual("hex", "default_hex"),
|
||||
|
||||
// Whether the saved value is different than Discourse's default color scheme.
|
||||
@discourseComputed("default_hex", "hex")
|
||||
savedIsOverriden(defaultHex) {
|
||||
return this.originals.hex !== defaultHex;
|
||||
},
|
||||
|
||||
revert() {
|
||||
this.set("hex", this.default_hex);
|
||||
},
|
||||
|
||||
undo() {
|
||||
if (this.originals) {
|
||||
this.set("hex", this.originals.hex);
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("name")
|
||||
translatedName(name) {
|
||||
if (!this.is_advanced) {
|
||||
return I18n.t(`admin.customize.colors.${name}.name`);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("name")
|
||||
description(name) {
|
||||
if (!this.is_advanced) {
|
||||
return I18n.t(`admin.customize.colors.${name}.description`);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
brightness returns a number between 0 (darkest) to 255 (brightest).
|
||||
Undefined if hex is not a valid color.
|
||||
|
||||
@property brightness
|
||||
**/
|
||||
@discourseComputed("hex")
|
||||
brightness(hex) {
|
||||
if (hex.length === 6 || hex.length === 3) {
|
||||
if (hex.length === 3) {
|
||||
hex =
|
||||
hex.substr(0, 1) +
|
||||
hex.substr(0, 1) +
|
||||
hex.substr(1, 1) +
|
||||
hex.substr(1, 1) +
|
||||
hex.substr(2, 1) +
|
||||
hex.substr(2, 1);
|
||||
}
|
||||
return Math.round(
|
||||
(parseInt(hex.substr(0, 2), 16) * 299 +
|
||||
parseInt(hex.substr(2, 2), 16) * 587 +
|
||||
parseInt(hex.substr(4, 2), 16) * 114) /
|
||||
1000
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@observes("hex")
|
||||
hexValueChanged() {
|
||||
if (this.hex) {
|
||||
this.set("hex", this.hex.toString().replace(/[^0-9a-fA-F]/g, ""));
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("hex")
|
||||
valid(hex) {
|
||||
return hex.match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null;
|
||||
},
|
||||
});
|
||||
|
||||
export default ColorSchemeColor;
|
||||
@@ -0,0 +1,170 @@
|
||||
import I18n from "I18n";
|
||||
import { A } from "@ember/array";
|
||||
import ArrayProxy from "@ember/array/proxy";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { not } from "@ember/object/computed";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import ColorSchemeColor from "admin/models/color-scheme-color";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const ColorScheme = EmberObject.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.startTrackingChanges();
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
description() {
|
||||
return "" + this.name;
|
||||
},
|
||||
|
||||
startTrackingChanges() {
|
||||
this.set("originals", { name: this.name });
|
||||
},
|
||||
|
||||
schemeJson() {
|
||||
const buffer = [];
|
||||
this.colors.forEach((c) => {
|
||||
buffer.push(` "${c.get("name")}": "${c.get("hex")}"`);
|
||||
});
|
||||
|
||||
return [`"${this.name}": {`, buffer.join(",\n"), "}"].join("\n");
|
||||
},
|
||||
|
||||
copy() {
|
||||
const newScheme = ColorScheme.create({
|
||||
name: this.name,
|
||||
can_edit: true,
|
||||
colors: A(),
|
||||
});
|
||||
this.colors.forEach((c) => {
|
||||
newScheme.colors.pushObject(
|
||||
ColorSchemeColor.create(c.getProperties("name", "hex", "default_hex"))
|
||||
);
|
||||
});
|
||||
return newScheme;
|
||||
},
|
||||
|
||||
@discourseComputed("name", "colors.@each.changed", "saving")
|
||||
changed(name) {
|
||||
if (!this.originals) {
|
||||
return false;
|
||||
}
|
||||
if (this.originals.name !== name) {
|
||||
return true;
|
||||
}
|
||||
if (this.colors.any((c) => c.get("changed"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
@discourseComputed("changed")
|
||||
disableSave(changed) {
|
||||
if (this.theme_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !changed || this.saving || this.colors.any((c) => !c.get("valid"));
|
||||
},
|
||||
|
||||
newRecord: not("id"),
|
||||
|
||||
save(opts) {
|
||||
if (this.is_base || this.disableSave) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setProperties({ savingStatus: I18n.t("saving"), saving: true });
|
||||
|
||||
const data = {};
|
||||
|
||||
if (!opts || !opts.enabledOnly) {
|
||||
data.name = this.name;
|
||||
data.base_scheme_id = this.base_scheme_id;
|
||||
data.colors = [];
|
||||
this.colors.forEach((c) => {
|
||||
if (!this.id || c.get("changed")) {
|
||||
data.colors.pushObject(c.getProperties("name", "hex"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return ajax(
|
||||
"/admin/color_schemes" + (this.id ? "/" + this.id : "") + ".json",
|
||||
{
|
||||
data: JSON.stringify({ color_scheme: data }),
|
||||
type: this.id ? "PUT" : "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
}
|
||||
).then((result) => {
|
||||
if (result.id) {
|
||||
this.set("id", result.id);
|
||||
}
|
||||
|
||||
if (!opts || !opts.enabledOnly) {
|
||||
this.startTrackingChanges();
|
||||
this.colors.forEach((c) => c.startTrackingChanges());
|
||||
}
|
||||
|
||||
this.setProperties({ savingStatus: I18n.t("saved"), saving: false });
|
||||
this.notifyPropertyChange("description");
|
||||
});
|
||||
},
|
||||
|
||||
updateUserSelectable(value) {
|
||||
if (!this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ajax(`/admin/color_schemes/${this.id}.json`, {
|
||||
data: JSON.stringify({ color_scheme: { user_selectable: value } }),
|
||||
type: "PUT",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this.id) {
|
||||
return ajax(`/admin/color_schemes/${this.id}`, { type: "DELETE" });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const ColorSchemes = ArrayProxy.extend({});
|
||||
|
||||
ColorScheme.reopenClass({
|
||||
findAll() {
|
||||
const colorSchemes = ColorSchemes.create({ content: [], loading: true });
|
||||
return ajax("/admin/color_schemes").then((all) => {
|
||||
all.forEach((colorScheme) => {
|
||||
colorSchemes.pushObject(
|
||||
ColorScheme.create({
|
||||
id: colorScheme.id,
|
||||
name: colorScheme.name,
|
||||
is_base: colorScheme.is_base,
|
||||
theme_id: colorScheme.theme_id,
|
||||
theme_name: colorScheme.theme_name,
|
||||
base_scheme_id: colorScheme.base_scheme_id,
|
||||
user_selectable: colorScheme.user_selectable,
|
||||
colors: colorScheme.colors.map((c) => {
|
||||
return ColorSchemeColor.create({
|
||||
name: c.name,
|
||||
hex: c.hex,
|
||||
default_hex: c.default_hex,
|
||||
is_advanced: c.is_advanced,
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
return colorSchemes;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default ColorScheme;
|
||||
@@ -0,0 +1,36 @@
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import AdminUser from "admin/models/admin-user";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const EmailLog = EmberObject.extend({});
|
||||
|
||||
EmailLog.reopenClass({
|
||||
create(attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
if (attrs.user) {
|
||||
attrs.user = AdminUser.create(attrs.user);
|
||||
}
|
||||
|
||||
if (attrs.post_url) {
|
||||
attrs.post_url = getURL(attrs.post_url);
|
||||
}
|
||||
|
||||
return this._super(attrs);
|
||||
},
|
||||
|
||||
findAll(filter, offset) {
|
||||
filter = filter || {};
|
||||
offset = offset || 0;
|
||||
|
||||
const status = filter.status || "sent";
|
||||
delete filter.status;
|
||||
|
||||
return ajax(`/admin/email/${status}.json?offset=${offset}`, {
|
||||
data: filter,
|
||||
}).then((logs) => logs.map((log) => EmailLog.create(log)));
|
||||
},
|
||||
});
|
||||
|
||||
export default EmailLog;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const EmailPreview = EmberObject.extend({});
|
||||
|
||||
export function oneWeekAgo() {
|
||||
return moment().locale("en").subtract(7, "days").format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
EmailPreview.reopenClass({
|
||||
findDigest(username, lastSeenAt) {
|
||||
return ajax("/admin/email/preview-digest.json", {
|
||||
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username },
|
||||
}).then((result) => EmailPreview.create(result));
|
||||
},
|
||||
|
||||
sendDigest(username, lastSeenAt, email) {
|
||||
return ajax("/admin/email/send-digest.json", {
|
||||
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username, email },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default EmailPreview;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const EmailSettings = EmberObject.extend({});
|
||||
|
||||
EmailSettings.reopenClass({
|
||||
find: function () {
|
||||
return ajax("/admin/email.json").then(function (settings) {
|
||||
return EmailSettings.create(settings);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default EmailSettings;
|
||||
@@ -0,0 +1,10 @@
|
||||
import RestModel from "discourse/models/rest";
|
||||
|
||||
export default RestModel.extend({
|
||||
changed: false,
|
||||
|
||||
setField(fieldName, value) {
|
||||
this.set(`${fieldName}`, value);
|
||||
this.set("changed", true);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import RestModel from "discourse/models/rest";
|
||||
const { getProperties } = Ember;
|
||||
|
||||
export default RestModel.extend({
|
||||
revert() {
|
||||
return ajax(`/admin/customize/email_templates/${this.id}`, {
|
||||
type: "DELETE",
|
||||
}).then((result) =>
|
||||
getProperties(result.email_template, "subject", "body", "can_revert")
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import RestModel from "discourse/models/rest";
|
||||
|
||||
export default RestModel.extend({
|
||||
@discourseComputed("id")
|
||||
name(id) {
|
||||
return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import AdminUser from "admin/models/admin-user";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const IncomingEmail = EmberObject.extend({});
|
||||
|
||||
IncomingEmail.reopenClass({
|
||||
create(attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
if (attrs.user) {
|
||||
attrs.user = AdminUser.create(attrs.user);
|
||||
}
|
||||
|
||||
return this._super(attrs);
|
||||
},
|
||||
|
||||
find(id) {
|
||||
return ajax(`/admin/email/incoming/${id}.json`);
|
||||
},
|
||||
|
||||
findByBounced(id) {
|
||||
return ajax(`/admin/email/incoming_from_bounced/${id}.json`);
|
||||
},
|
||||
|
||||
findAll(filter, offset) {
|
||||
filter = filter || {};
|
||||
offset = offset || 0;
|
||||
|
||||
const status = filter.status || "received";
|
||||
delete filter.status;
|
||||
|
||||
return ajax(`/admin/email/${status}.json?offset=${offset}`, {
|
||||
data: filter,
|
||||
}).then((incomings) =>
|
||||
incomings.map((incoming) => IncomingEmail.create(incoming))
|
||||
);
|
||||
},
|
||||
|
||||
loadRawEmail(id) {
|
||||
return ajax(`/admin/email/incoming/${id}/raw.json`);
|
||||
},
|
||||
});
|
||||
|
||||
export default IncomingEmail;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Category from "discourse/models/category";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const Permalink = EmberObject.extend({
|
||||
save: function () {
|
||||
return ajax("/admin/permalinks.json", {
|
||||
type: "POST",
|
||||
data: {
|
||||
url: this.url,
|
||||
permalink_type: this.permalink_type,
|
||||
permalink_type_value: this.permalink_type_value,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("category_id")
|
||||
category: function (category_id) {
|
||||
return Category.findById(category_id);
|
||||
},
|
||||
|
||||
@discourseComputed("external_url")
|
||||
linkIsExternal: function (external_url) {
|
||||
return !DiscourseURL.isInternal(external_url);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
return ajax("/admin/permalinks/" + this.id + ".json", {
|
||||
type: "DELETE",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Permalink.reopenClass({
|
||||
findAll: function (filter) {
|
||||
return ajax("/admin/permalinks.json", { data: { filter: filter } }).then(
|
||||
function (permalinks) {
|
||||
return permalinks.map((p) => Permalink.create(p));
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default Permalink;
|
||||
@@ -0,0 +1,578 @@
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import EmberObject from "@ember/object";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import round from "discourse/lib/round";
|
||||
import {
|
||||
fillMissingDates,
|
||||
formatUsername,
|
||||
toNumber,
|
||||
} from "discourse/lib/utilities";
|
||||
import { number, durationTiny } from "discourse/lib/formatter";
|
||||
import { renderAvatar } from "discourse/helpers/user-avatar";
|
||||
|
||||
// Change this line each time report format change
|
||||
// and you want to ensure cache is reset
|
||||
export const SCHEMA_VERSION = 4;
|
||||
|
||||
const Report = EmberObject.extend({
|
||||
average: false,
|
||||
percent: false,
|
||||
higher_is_better: true,
|
||||
description_link: null,
|
||||
description: null,
|
||||
|
||||
@discourseComputed("type", "start_date", "end_date")
|
||||
reportUrl(type, start_date, end_date) {
|
||||
start_date = moment.utc(start_date).locale("en").format("YYYY-MM-DD");
|
||||
|
||||
end_date = moment.utc(end_date).locale("en").format("YYYY-MM-DD");
|
||||
|
||||
return getURL(
|
||||
`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`
|
||||
);
|
||||
},
|
||||
|
||||
valueAt(numDaysAgo) {
|
||||
if (this.data) {
|
||||
const wantedDate = moment()
|
||||
.subtract(numDaysAgo, "days")
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
const item = this.data.find((d) => d.x === wantedDate);
|
||||
if (item) {
|
||||
return item.y;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
valueFor(startDaysAgo, endDaysAgo) {
|
||||
if (this.data) {
|
||||
const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day");
|
||||
const latestDate = moment().subtract(startDaysAgo, "days").startOf("day");
|
||||
let d,
|
||||
sum = 0,
|
||||
count = 0;
|
||||
this.data.forEach((datum) => {
|
||||
d = moment(datum.x);
|
||||
if (d >= earliestDate && d <= latestDate) {
|
||||
sum += datum.y;
|
||||
count++;
|
||||
}
|
||||
});
|
||||
if (this.method === "average" && count > 0) {
|
||||
sum /= count;
|
||||
}
|
||||
return round(sum, -2);
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("data", "average")
|
||||
todayCount() {
|
||||
return this.valueAt(0);
|
||||
},
|
||||
|
||||
@discourseComputed("data", "average")
|
||||
yesterdayCount() {
|
||||
return this.valueAt(1);
|
||||
},
|
||||
|
||||
@discourseComputed("data", "average")
|
||||
sevenDaysAgoCount() {
|
||||
return this.valueAt(7);
|
||||
},
|
||||
|
||||
@discourseComputed("data", "average")
|
||||
thirtyDaysAgoCount() {
|
||||
return this.valueAt(30);
|
||||
},
|
||||
|
||||
@discourseComputed("data", "average")
|
||||
lastSevenDaysCount() {
|
||||
return this.averageCount(7, this.valueFor(1, 7));
|
||||
},
|
||||
|
||||
@discourseComputed("data", "average")
|
||||
lastThirtyDaysCount() {
|
||||
return this.averageCount(30, this.valueFor(1, 30));
|
||||
},
|
||||
|
||||
averageCount(count, value) {
|
||||
return this.average ? value / count : value;
|
||||
},
|
||||
|
||||
@discourseComputed("yesterdayCount", "higher_is_better")
|
||||
yesterdayTrend(yesterdayCount, higherIsBetter) {
|
||||
return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter);
|
||||
},
|
||||
|
||||
@discourseComputed("lastSevenDaysCount", "higher_is_better")
|
||||
sevenDaysTrend(lastSevenDaysCount, higherIsBetter) {
|
||||
return this._computeTrend(
|
||||
this.valueFor(8, 14),
|
||||
lastSevenDaysCount,
|
||||
higherIsBetter
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("data")
|
||||
currentTotal(data) {
|
||||
return data.reduce((cur, pair) => cur + pair.y, 0);
|
||||
},
|
||||
|
||||
@discourseComputed("data", "currentTotal")
|
||||
currentAverage(data, total) {
|
||||
return makeArray(data).length === 0
|
||||
? 0
|
||||
: parseFloat((total / parseFloat(data.length)).toFixed(1));
|
||||
},
|
||||
|
||||
@discourseComputed("trend", "higher_is_better")
|
||||
trendIcon(trend, higherIsBetter) {
|
||||
return this._iconForTrend(trend, higherIsBetter);
|
||||
},
|
||||
|
||||
@discourseComputed("sevenDaysTrend", "higher_is_better")
|
||||
sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) {
|
||||
return this._iconForTrend(sevenDaysTrend, higherIsBetter);
|
||||
},
|
||||
|
||||
@discourseComputed("thirtyDaysTrend", "higher_is_better")
|
||||
thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) {
|
||||
return this._iconForTrend(thirtyDaysTrend, higherIsBetter);
|
||||
},
|
||||
|
||||
@discourseComputed("yesterdayTrend", "higher_is_better")
|
||||
yesterdayTrendIcon(yesterdayTrend, higherIsBetter) {
|
||||
return this._iconForTrend(yesterdayTrend, higherIsBetter);
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"prev_period",
|
||||
"currentTotal",
|
||||
"currentAverage",
|
||||
"higher_is_better"
|
||||
)
|
||||
trend(prev, currentTotal, currentAverage, higherIsBetter) {
|
||||
const total = this.average ? currentAverage : currentTotal;
|
||||
return this._computeTrend(prev, total, higherIsBetter);
|
||||
},
|
||||
|
||||
@discourseComputed("prev30Days", "lastThirtyDaysCount", "higher_is_better")
|
||||
thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) {
|
||||
return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter);
|
||||
},
|
||||
|
||||
@discourseComputed("type")
|
||||
method(type) {
|
||||
if (type === "time_to_first_response") {
|
||||
return "average";
|
||||
} else {
|
||||
return "sum";
|
||||
}
|
||||
},
|
||||
|
||||
percentChangeString(val1, val2) {
|
||||
const change = this._computeChange(val1, val2);
|
||||
|
||||
if (isNaN(change) || !isFinite(change)) {
|
||||
return null;
|
||||
} else if (change > 0) {
|
||||
return "+" + change.toFixed(0) + "%";
|
||||
} else {
|
||||
return change.toFixed(0) + "%";
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("prev_period", "currentTotal", "currentAverage")
|
||||
trendTitle(prev, currentTotal, currentAverage) {
|
||||
let current = this.average ? currentAverage : currentTotal;
|
||||
let percent = this.percentChangeString(prev, current);
|
||||
|
||||
if (this.average) {
|
||||
prev = prev ? prev.toFixed(1) : "0";
|
||||
if (this.percent) {
|
||||
current += "%";
|
||||
prev += "%";
|
||||
}
|
||||
} else {
|
||||
prev = number(prev);
|
||||
current = number(current);
|
||||
}
|
||||
|
||||
return I18n.t("admin.dashboard.reports.trend_title", {
|
||||
percent,
|
||||
prev,
|
||||
current,
|
||||
});
|
||||
},
|
||||
|
||||
changeTitle(valAtT1, valAtT2, prevPeriodString) {
|
||||
const change = this.percentChangeString(valAtT1, valAtT2);
|
||||
let title = "";
|
||||
if (change) {
|
||||
title += `${change} change. `;
|
||||
}
|
||||
title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
|
||||
return title;
|
||||
},
|
||||
|
||||
@discourseComputed("yesterdayCount")
|
||||
yesterdayCountTitle(yesterdayCount) {
|
||||
return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
|
||||
},
|
||||
|
||||
@discourseComputed("lastSevenDaysCount")
|
||||
sevenDaysCountTitle(lastSevenDaysCount) {
|
||||
return this.changeTitle(
|
||||
this.valueFor(8, 14),
|
||||
lastSevenDaysCount,
|
||||
"two weeks ago"
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("prev30Days", "lastThirtyDaysCount")
|
||||
thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) {
|
||||
return this.changeTitle(
|
||||
prev30Days,
|
||||
lastThirtyDaysCount,
|
||||
"in the previous 30 day period"
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("data")
|
||||
sortedData(data) {
|
||||
return this.xAxisIsDate ? data.toArray().reverse() : data.toArray();
|
||||
},
|
||||
|
||||
@discourseComputed("data")
|
||||
xAxisIsDate() {
|
||||
if (!this.data[0]) {
|
||||
return false;
|
||||
}
|
||||
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
||||
},
|
||||
|
||||
@discourseComputed("labels")
|
||||
computedLabels(labels) {
|
||||
return labels.map((label) => {
|
||||
const type = label.type || "string";
|
||||
|
||||
let mainProperty;
|
||||
if (label.property) {
|
||||
mainProperty = label.property;
|
||||
} else if (type === "user") {
|
||||
mainProperty = label.properties["username"];
|
||||
} else if (type === "topic") {
|
||||
mainProperty = label.properties["title"];
|
||||
} else if (type === "post") {
|
||||
mainProperty = label.properties["truncated_raw"];
|
||||
} else {
|
||||
mainProperty = label.properties[0];
|
||||
}
|
||||
|
||||
return {
|
||||
title: label.title,
|
||||
sortProperty: label.sort_property || mainProperty,
|
||||
mainProperty,
|
||||
type,
|
||||
compute: (row, opts = {}) => {
|
||||
let value = null;
|
||||
|
||||
if (opts.useSortProperty) {
|
||||
value = row[label.sort_property || mainProperty];
|
||||
} else {
|
||||
value = row[mainProperty];
|
||||
}
|
||||
|
||||
if (type === "user") {
|
||||
return this._userLabel(label.properties, row);
|
||||
}
|
||||
if (type === "post") {
|
||||
return this._postLabel(label.properties, row);
|
||||
}
|
||||
if (type === "topic") {
|
||||
return this._topicLabel(label.properties, row);
|
||||
}
|
||||
if (type === "seconds") {
|
||||
return this._secondsLabel(value);
|
||||
}
|
||||
if (type === "link") {
|
||||
return this._linkLabel(label.properties, row);
|
||||
}
|
||||
if (type === "percent") {
|
||||
return this._percentLabel(value);
|
||||
}
|
||||
if (type === "bytes") {
|
||||
return this._bytesLabel(value);
|
||||
}
|
||||
if (type === "number") {
|
||||
return this._numberLabel(value, opts);
|
||||
}
|
||||
if (type === "date") {
|
||||
const date = moment(value);
|
||||
if (date.isValid()) {
|
||||
return this._dateLabel(value, date);
|
||||
}
|
||||
}
|
||||
if (type === "precise_date") {
|
||||
const date = moment(value);
|
||||
if (date.isValid()) {
|
||||
return this._dateLabel(value, date, "LLL");
|
||||
}
|
||||
}
|
||||
if (type === "text") {
|
||||
return this._textLabel(value);
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
type,
|
||||
property: mainProperty,
|
||||
formatedValue: value ? escapeExpression(value) : "—",
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
_userLabel(properties, row) {
|
||||
const username = row[properties.username];
|
||||
|
||||
const formatedValue = () => {
|
||||
const userId = row[properties.id];
|
||||
|
||||
const user = EmberObject.create({
|
||||
username,
|
||||
name: formatUsername(username),
|
||||
avatar_template: row[properties.avatar],
|
||||
});
|
||||
|
||||
const href = getURL(`/admin/users/${userId}/${username}`);
|
||||
|
||||
const avatarImg = renderAvatar(user, {
|
||||
imageSize: "tiny",
|
||||
ignoreTitle: true,
|
||||
siteSettings: this.siteSettings,
|
||||
});
|
||||
|
||||
return `<a href='${href}'>${avatarImg}<span class='username'>${user.name}</span></a>`;
|
||||
};
|
||||
|
||||
return {
|
||||
value: username,
|
||||
formatedValue: username ? formatedValue() : "—",
|
||||
};
|
||||
},
|
||||
|
||||
_topicLabel(properties, row) {
|
||||
const topicTitle = row[properties.title];
|
||||
|
||||
const formatedValue = () => {
|
||||
const topicId = row[properties.id];
|
||||
const href = getURL(`/t/-/${topicId}`);
|
||||
return `<a href='${href}'>${escapeExpression(topicTitle)}</a>`;
|
||||
};
|
||||
|
||||
return {
|
||||
value: topicTitle,
|
||||
formatedValue: topicTitle ? formatedValue() : "—",
|
||||
};
|
||||
},
|
||||
|
||||
_postLabel(properties, row) {
|
||||
const postTitle = row[properties.truncated_raw];
|
||||
const postNumber = row[properties.number];
|
||||
const topicId = row[properties.topic_id];
|
||||
const href = getURL(`/t/-/${topicId}/${postNumber}`);
|
||||
|
||||
return {
|
||||
property: properties.title,
|
||||
value: postTitle,
|
||||
formatedValue:
|
||||
postTitle && href
|
||||
? `<a href='${href}'>${escapeExpression(postTitle)}</a>`
|
||||
: "—",
|
||||
};
|
||||
},
|
||||
|
||||
_secondsLabel(value) {
|
||||
return {
|
||||
value: toNumber(value),
|
||||
formatedValue: durationTiny(value),
|
||||
};
|
||||
},
|
||||
|
||||
_percentLabel(value) {
|
||||
return {
|
||||
value: toNumber(value),
|
||||
formatedValue: value ? `${value}%` : "—",
|
||||
};
|
||||
},
|
||||
|
||||
_numberLabel(value, options = {}) {
|
||||
const formatNumbers = isEmpty(options.formatNumbers)
|
||||
? true
|
||||
: options.formatNumbers;
|
||||
|
||||
const formatedValue = () => (formatNumbers ? number(value) : value);
|
||||
|
||||
return {
|
||||
value: toNumber(value),
|
||||
formatedValue: value ? formatedValue() : "—",
|
||||
};
|
||||
},
|
||||
|
||||
_bytesLabel(value) {
|
||||
return {
|
||||
value: toNumber(value),
|
||||
formatedValue: I18n.toHumanSize(value),
|
||||
};
|
||||
},
|
||||
|
||||
_dateLabel(value, date, format = "LL") {
|
||||
return {
|
||||
value,
|
||||
formatedValue: value ? date.format(format) : "—",
|
||||
};
|
||||
},
|
||||
|
||||
_textLabel(value) {
|
||||
const escaped = escapeExpression(value);
|
||||
|
||||
return {
|
||||
value,
|
||||
formatedValue: value ? escaped : "—",
|
||||
};
|
||||
},
|
||||
|
||||
_linkLabel(properties, row) {
|
||||
const property = properties[0];
|
||||
const value = getURL(row[property]);
|
||||
const formatedValue = (href, anchor) => {
|
||||
return `<a href="${escapeExpression(href)}">${escapeExpression(
|
||||
anchor
|
||||
)}</a>`;
|
||||
};
|
||||
|
||||
return {
|
||||
value,
|
||||
formatedValue: value ? formatedValue(value, row[properties[1]]) : "—",
|
||||
};
|
||||
},
|
||||
|
||||
_computeChange(valAtT1, valAtT2) {
|
||||
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
||||
},
|
||||
|
||||
_computeTrend(valAtT1, valAtT2, higherIsBetter) {
|
||||
const change = this._computeChange(valAtT1, valAtT2);
|
||||
|
||||
if (change > 50) {
|
||||
return higherIsBetter ? "high-trending-up" : "high-trending-down";
|
||||
} else if (change > 2) {
|
||||
return higherIsBetter ? "trending-up" : "trending-down";
|
||||
} else if (change <= 2 && change >= -2) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return higherIsBetter ? "high-trending-down" : "high-trending-up";
|
||||
} else if (change < -2) {
|
||||
return higherIsBetter ? "trending-down" : "trending-up";
|
||||
}
|
||||
},
|
||||
|
||||
_iconForTrend(trend, higherIsBetter) {
|
||||
switch (trend) {
|
||||
case "trending-up":
|
||||
return higherIsBetter ? "angle-up" : "angle-down";
|
||||
case "trending-down":
|
||||
return higherIsBetter ? "angle-down" : "angle-up";
|
||||
case "high-trending-up":
|
||||
return higherIsBetter ? "angle-double-up" : "angle-double-down";
|
||||
case "high-trending-down":
|
||||
return higherIsBetter ? "angle-double-down" : "angle-double-up";
|
||||
default:
|
||||
return "minus";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Report.reopenClass({
|
||||
fillMissingDates(report, options = {}) {
|
||||
const dataField = options.dataField || "data";
|
||||
const filledField = options.filledField || "data";
|
||||
const startDate = options.startDate || "start_date";
|
||||
const endDate = options.endDate || "end_date";
|
||||
|
||||
if (Array.isArray(report[dataField])) {
|
||||
const startDateFormatted = moment
|
||||
.utc(report[startDate])
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
const endDateFormatted = moment
|
||||
.utc(report[endDate])
|
||||
.locale("en")
|
||||
.format("YYYY-MM-DD");
|
||||
|
||||
if (report.modes[0] === "stacked_chart") {
|
||||
report[filledField] = report[dataField].map((rep) => {
|
||||
return {
|
||||
req: rep.req,
|
||||
label: rep.label,
|
||||
color: rep.color,
|
||||
data: fillMissingDates(
|
||||
JSON.parse(JSON.stringify(rep.data)),
|
||||
startDateFormatted,
|
||||
endDateFormatted
|
||||
),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
report[filledField] = fillMissingDates(
|
||||
JSON.parse(JSON.stringify(report[dataField])),
|
||||
startDateFormatted,
|
||||
endDateFormatted
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
find(type, startDate, endDate, categoryId, groupId) {
|
||||
return ajax("/admin/reports/" + type, {
|
||||
data: {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
category_id: categoryId,
|
||||
group_id: groupId,
|
||||
},
|
||||
}).then((json) => {
|
||||
// don’t fill for large multi column tables
|
||||
// which are not date based
|
||||
const modes = json.report.modes;
|
||||
if (modes.length !== 1 && modes[0] !== "table") {
|
||||
Report.fillMissingDates(json.report);
|
||||
}
|
||||
|
||||
const model = Report.create({ type: type });
|
||||
model.setProperties(json.report);
|
||||
|
||||
if (json.report.related_report) {
|
||||
// TODO: fillMissingDates if xaxis is date
|
||||
const related = Report.create({
|
||||
type: json.report.related_report.type,
|
||||
});
|
||||
related.setProperties(json.report.related_report);
|
||||
model.set("relatedReport", related);
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default Report;
|
||||
@@ -0,0 +1,31 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const ScreenedEmail = EmberObject.extend({
|
||||
@discourseComputed("action")
|
||||
actionName(action) {
|
||||
return I18n.t("admin.logs.screened_actions." + action);
|
||||
},
|
||||
|
||||
clearBlock: function () {
|
||||
return ajax("/admin/logs/screened_emails/" + this.id, {
|
||||
type: "DELETE",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
ScreenedEmail.reopenClass({
|
||||
findAll: function () {
|
||||
return ajax("/admin/logs/screened_emails.json").then(function (
|
||||
screened_emails
|
||||
) {
|
||||
return screened_emails.map(function (b) {
|
||||
return ScreenedEmail.create(b);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default ScreenedEmail;
|
||||
@@ -0,0 +1,56 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const ScreenedIpAddress = EmberObject.extend({
|
||||
@discourseComputed("action_name")
|
||||
actionName(actionName) {
|
||||
return I18n.t(`admin.logs.screened_ips.actions.${actionName}`);
|
||||
},
|
||||
|
||||
isBlocked: equal("action_name", "block"),
|
||||
|
||||
@discourseComputed("ip_address")
|
||||
isRange(ipAddress) {
|
||||
return ipAddress.indexOf("/") > 0;
|
||||
},
|
||||
|
||||
save() {
|
||||
return ajax(
|
||||
"/admin/logs/screened_ip_addresses" +
|
||||
(this.id ? "/" + this.id : "") +
|
||||
".json",
|
||||
{
|
||||
type: this.id ? "PUT" : "POST",
|
||||
data: {
|
||||
ip_address: this.ip_address,
|
||||
action_name: this.action_name,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
destroy() {
|
||||
return ajax("/admin/logs/screened_ip_addresses/" + this.id + ".json", {
|
||||
type: "DELETE",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
ScreenedIpAddress.reopenClass({
|
||||
findAll(filter) {
|
||||
return ajax("/admin/logs/screened_ip_addresses.json", {
|
||||
data: { filter: filter },
|
||||
}).then((screened_ips) =>
|
||||
screened_ips.map((b) => ScreenedIpAddress.create(b))
|
||||
);
|
||||
},
|
||||
|
||||
rollUp() {
|
||||
return ajax("/admin/logs/screened_ip_addresses/roll_up", { type: "POST" });
|
||||
},
|
||||
});
|
||||
|
||||
export default ScreenedIpAddress;
|
||||
@@ -0,0 +1,25 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const ScreenedUrl = EmberObject.extend({
|
||||
@discourseComputed("action")
|
||||
actionName(action) {
|
||||
return I18n.t("admin.logs.screened_actions." + action);
|
||||
},
|
||||
});
|
||||
|
||||
ScreenedUrl.reopenClass({
|
||||
findAll: function () {
|
||||
return ajax("/admin/logs/screened_urls.json").then(function (
|
||||
screened_urls
|
||||
) {
|
||||
return screened_urls.map(function (b) {
|
||||
return ScreenedUrl.create(b);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default ScreenedUrl;
|
||||
@@ -0,0 +1,42 @@
|
||||
import I18n from "I18n";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import Setting from "admin/mixins/setting-object";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const SiteSetting = EmberObject.extend(Setting, {});
|
||||
|
||||
SiteSetting.reopenClass({
|
||||
findAll() {
|
||||
return ajax("/admin/site_settings").then(function (settings) {
|
||||
// Group the results by category
|
||||
const categories = {};
|
||||
settings.site_settings.forEach(function (s) {
|
||||
if (!categories[s.category]) {
|
||||
categories[s.category] = [];
|
||||
}
|
||||
categories[s.category].pushObject(SiteSetting.create(s));
|
||||
});
|
||||
|
||||
return Object.keys(categories).map(function (n) {
|
||||
return {
|
||||
nameKey: n,
|
||||
name: I18n.t("admin.site_settings.categories." + n),
|
||||
siteSettings: categories[n],
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
update(key, value, opts = {}) {
|
||||
const data = {};
|
||||
data[key] = value;
|
||||
|
||||
if (opts["updateExistingUsers"] === true) {
|
||||
data["update_existing_user"] = true;
|
||||
}
|
||||
|
||||
return ajax(`/admin/site_settings/${key}`, { type: "PUT", data });
|
||||
},
|
||||
});
|
||||
|
||||
export default SiteSetting;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import RestModel from "discourse/models/rest";
|
||||
const { getProperties } = Ember;
|
||||
|
||||
export default RestModel.extend({
|
||||
revert() {
|
||||
return ajax(`/admin/customize/site_texts/${this.id}`, {
|
||||
type: "DELETE",
|
||||
}).then((result) => getProperties(result.site_text, "value", "can_revert"));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import AdminUser from "admin/models/admin-user";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import RestModel from "discourse/models/rest";
|
||||
|
||||
function format(label, value, escape = true) {
|
||||
return value
|
||||
? `<b>${I18n.t(label)}</b>: ${escape ? escapeExpression(value) : value}`
|
||||
: "";
|
||||
}
|
||||
|
||||
const StaffActionLog = RestModel.extend({
|
||||
showFullDetails: false,
|
||||
|
||||
@discourseComputed("action_name")
|
||||
actionName(actionName) {
|
||||
return I18n.t(`admin.logs.staff_actions.actions.${actionName}`);
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"email",
|
||||
"ip_address",
|
||||
"topic_id",
|
||||
"post_id",
|
||||
"category_id",
|
||||
"new_value",
|
||||
"previous_value",
|
||||
"details",
|
||||
"useCustomModalForDetails",
|
||||
"useModalForDetails"
|
||||
)
|
||||
formattedDetails(
|
||||
email,
|
||||
ipAddress,
|
||||
topicId,
|
||||
postId,
|
||||
categoryId,
|
||||
newValue,
|
||||
previousValue,
|
||||
details,
|
||||
useCustomModalForDetails,
|
||||
useModalForDetails
|
||||
) {
|
||||
const postLink = postId
|
||||
? `<a href data-link-post-id="${postId}">${postId}</a>`
|
||||
: null;
|
||||
|
||||
const topicLink = topicId
|
||||
? `<a href data-link-topic-id="${topicId}">${topicId}</a>`
|
||||
: null;
|
||||
|
||||
let lines = [
|
||||
format("email", email),
|
||||
format("admin.logs.ip_address", ipAddress),
|
||||
format("admin.logs.topic_id", topicLink, false),
|
||||
format("admin.logs.post_id", postLink, false),
|
||||
format("admin.logs.category_id", categoryId),
|
||||
];
|
||||
|
||||
if (!useCustomModalForDetails) {
|
||||
lines.push(format("admin.logs.staff_actions.new_value", newValue));
|
||||
lines.push(
|
||||
format("admin.logs.staff_actions.previous_value", previousValue)
|
||||
);
|
||||
}
|
||||
|
||||
if (!useModalForDetails && details) {
|
||||
lines = [...lines, ...escapeExpression(details).split("\n")];
|
||||
}
|
||||
|
||||
const formatted = lines.filter((l) => l.length > 0).join("<br/>");
|
||||
return formatted.length > 0 ? formatted + "<br/>" : "";
|
||||
},
|
||||
|
||||
@discourseComputed("details")
|
||||
useModalForDetails(details) {
|
||||
return details && details.length > 100;
|
||||
},
|
||||
|
||||
@discourseComputed("action_name")
|
||||
useCustomModalForDetails(actionName) {
|
||||
return ["change_theme", "delete_theme"].includes(actionName);
|
||||
},
|
||||
});
|
||||
|
||||
StaffActionLog.reopenClass({
|
||||
munge(json) {
|
||||
if (json.acting_user) {
|
||||
json.acting_user = AdminUser.create(json.acting_user);
|
||||
}
|
||||
if (json.target_user) {
|
||||
json.target_user = AdminUser.create(json.target_user);
|
||||
}
|
||||
return json;
|
||||
},
|
||||
|
||||
findAll(data) {
|
||||
return ajax("/admin/logs/staff_action_logs.json", { data }).then(
|
||||
(result) => {
|
||||
return {
|
||||
staff_action_logs: result.staff_action_logs.map((s) =>
|
||||
StaffActionLog.create(s)
|
||||
),
|
||||
user_history_actions: result.user_history_actions,
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default StaffActionLog;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Setting from "admin/mixins/setting-object";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
export default EmberObject.extend(Setting, {});
|
||||
@@ -0,0 +1,348 @@
|
||||
import I18n from "I18n";
|
||||
import { get } from "@ember/object";
|
||||
import { isBlank, isEmpty } from "@ember/utils";
|
||||
import { or, gt } from "@ember/object/computed";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||
import { url } from "discourse/lib/computed";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
const FIELDS_IDS = [0, 1, 5];
|
||||
|
||||
export const THEMES = "themes";
|
||||
export const COMPONENTS = "components";
|
||||
const SETTINGS_TYPE_ID = 5;
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
isActive: or("default", "user_selectable"),
|
||||
isPendingUpdates: gt("remote_theme.commits_behind", 0),
|
||||
hasEditedFields: gt("editedFields.length", 0),
|
||||
hasParents: gt("parent_themes.length", 0),
|
||||
diffLocalChangesUrl: url("id", "/admin/themes/%@/diff_local_changes"),
|
||||
|
||||
@discourseComputed("theme_fields.[]")
|
||||
targets() {
|
||||
return [
|
||||
{ id: 0, name: "common" },
|
||||
{ id: 1, name: "desktop", icon: "desktop" },
|
||||
{ id: 2, name: "mobile", icon: "mobile-alt" },
|
||||
{ id: 3, name: "settings", icon: "cog", advanced: true },
|
||||
{
|
||||
id: 4,
|
||||
name: "translations",
|
||||
icon: "globe",
|
||||
advanced: true,
|
||||
customNames: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "extra_scss",
|
||||
icon: "paint-brush",
|
||||
advanced: true,
|
||||
customNames: true,
|
||||
},
|
||||
].map((target) => {
|
||||
target["edited"] = this.hasEdited(target.name);
|
||||
target["error"] = this.hasError(target.name);
|
||||
return target;
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("theme_fields.[]")
|
||||
fieldNames() {
|
||||
const common = [
|
||||
"scss",
|
||||
"head_tag",
|
||||
"header",
|
||||
"after_header",
|
||||
"body_tag",
|
||||
"footer",
|
||||
];
|
||||
|
||||
const scss_fields = (this.theme_fields || [])
|
||||
.filter((f) => f.target === "extra_scss" && f.name !== "")
|
||||
.map((f) => f.name);
|
||||
|
||||
if (scss_fields.length < 1) {
|
||||
scss_fields.push("importable_scss");
|
||||
}
|
||||
|
||||
return {
|
||||
common: [...common, "embedded_scss", "color_definitions"],
|
||||
desktop: common,
|
||||
mobile: common,
|
||||
settings: ["yaml"],
|
||||
translations: [
|
||||
"en",
|
||||
...(this.theme_fields || [])
|
||||
.filter((f) => f.target === "translations" && f.name !== "en")
|
||||
.map((f) => f.name),
|
||||
],
|
||||
extra_scss: scss_fields,
|
||||
};
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"fieldNames",
|
||||
"theme_fields.[]",
|
||||
"theme_fields.@each.error"
|
||||
)
|
||||
fields(fieldNames) {
|
||||
const hash = {};
|
||||
Object.keys(fieldNames).forEach((target) => {
|
||||
hash[target] = fieldNames[target].map((fieldName) => {
|
||||
const field = {
|
||||
name: fieldName,
|
||||
edited: this.hasEdited(target, fieldName),
|
||||
error: this.hasError(target, fieldName),
|
||||
};
|
||||
|
||||
if (target === "translations" || target === "extra_scss") {
|
||||
field.translatedName = fieldName;
|
||||
} else {
|
||||
field.translatedName = I18n.t(
|
||||
`admin.customize.theme.${fieldName}.text`
|
||||
);
|
||||
field.title = I18n.t(`admin.customize.theme.${fieldName}.title`);
|
||||
}
|
||||
|
||||
if (fieldName.indexOf("_tag") > 0) {
|
||||
field.icon = "far-file-alt";
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
});
|
||||
return hash;
|
||||
},
|
||||
|
||||
@discourseComputed("theme_fields")
|
||||
themeFields(fields) {
|
||||
if (!fields) {
|
||||
this.set("theme_fields", []);
|
||||
return {};
|
||||
}
|
||||
|
||||
let hash = {};
|
||||
fields.forEach((field) => {
|
||||
if (!field.type_id || FIELDS_IDS.includes(field.type_id)) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
return hash;
|
||||
},
|
||||
|
||||
@discourseComputed("theme_fields", "theme_fields.[]")
|
||||
uploads(fields) {
|
||||
if (!fields) {
|
||||
return [];
|
||||
}
|
||||
return fields.filter(
|
||||
(f) => f.target === "common" && f.type_id === THEME_UPLOAD_VAR
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("theme_fields", "theme_fields.@each.error")
|
||||
isBroken(fields) {
|
||||
return (
|
||||
fields && fields.any((field) => field.error && field.error.length > 0)
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("theme_fields.[]")
|
||||
editedFields(fields) {
|
||||
return fields.filter(
|
||||
(field) => !isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("remote_theme.last_error_text")
|
||||
remoteError(errorText) {
|
||||
if (errorText && errorText.length > 0) {
|
||||
return errorText;
|
||||
}
|
||||
},
|
||||
|
||||
getKey(field) {
|
||||
return `${field.target} ${field.name}`;
|
||||
},
|
||||
|
||||
hasEdited(target, name) {
|
||||
if (name) {
|
||||
return !isEmpty(this.getField(target, name));
|
||||
} else {
|
||||
let fields = this.theme_fields || [];
|
||||
return fields.any(
|
||||
(field) => field.target === target && !isEmpty(field.value)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
hasError(target, name) {
|
||||
return this.theme_fields
|
||||
.filter((f) => f.target === target && (!name || name === f.name))
|
||||
.any((f) => f.error);
|
||||
},
|
||||
|
||||
getError(target, name) {
|
||||
let themeFields = this.themeFields;
|
||||
let key = this.getKey({ target, name });
|
||||
let field = themeFields[key];
|
||||
return field ? field.error : "";
|
||||
},
|
||||
|
||||
getField(target, name) {
|
||||
let themeFields = this.themeFields;
|
||||
let key = this.getKey({ target, name });
|
||||
let field = themeFields[key];
|
||||
return field ? field.value : "";
|
||||
},
|
||||
|
||||
removeField(field) {
|
||||
this.set("changed", true);
|
||||
|
||||
field.upload_id = null;
|
||||
field.value = null;
|
||||
|
||||
return this.saveChanges("theme_fields");
|
||||
},
|
||||
|
||||
setField(target, name, value, upload_id, type_id) {
|
||||
this.set("changed", true);
|
||||
let themeFields = this.themeFields;
|
||||
let field = { name, target, value, upload_id, type_id };
|
||||
|
||||
// slow path for uploads and so on
|
||||
if (type_id && type_id > 1) {
|
||||
let fields = this.theme_fields;
|
||||
let existing = fields.find(
|
||||
(f) => f.target === target && f.name === name && f.type_id === type_id
|
||||
);
|
||||
if (existing) {
|
||||
existing.value = value;
|
||||
existing.upload_id = upload_id;
|
||||
} else {
|
||||
fields.pushObject(field);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// fast path
|
||||
let key = this.getKey({ target, name });
|
||||
let existingField = themeFields[key];
|
||||
if (!existingField) {
|
||||
this.theme_fields.pushObject(field);
|
||||
themeFields[key] = field;
|
||||
} else {
|
||||
const changed =
|
||||
(isEmpty(existingField.value) && !isEmpty(value)) ||
|
||||
(isEmpty(value) && !isEmpty(existingField.value));
|
||||
|
||||
existingField.value = value;
|
||||
if (changed) {
|
||||
// Observing theme_fields.@each.value is too slow, so manually notify
|
||||
// if the value goes to/from blank
|
||||
this.notifyPropertyChange("theme_fields.[]");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("childThemes.[]")
|
||||
child_theme_ids(childThemes) {
|
||||
if (childThemes) {
|
||||
return childThemes.map((theme) => get(theme, "id"));
|
||||
}
|
||||
},
|
||||
|
||||
removeChildTheme(theme) {
|
||||
const childThemes = this.childThemes;
|
||||
childThemes.removeObject(theme);
|
||||
return this.saveChanges("child_theme_ids");
|
||||
},
|
||||
|
||||
addChildTheme(theme) {
|
||||
let childThemes = this.childThemes;
|
||||
if (!childThemes) {
|
||||
childThemes = [];
|
||||
this.set("childThemes", childThemes);
|
||||
}
|
||||
childThemes.removeObject(theme);
|
||||
childThemes.pushObject(theme);
|
||||
return this.saveChanges("child_theme_ids");
|
||||
},
|
||||
|
||||
addParentTheme(theme) {
|
||||
let parentThemes = this.parentThemes;
|
||||
if (!parentThemes) {
|
||||
parentThemes = [];
|
||||
this.set("parentThemes", parentThemes);
|
||||
}
|
||||
parentThemes.addObject(theme);
|
||||
},
|
||||
|
||||
checkForUpdates() {
|
||||
return this.save({ remote_check: true }).then(() =>
|
||||
this.set("changed", false)
|
||||
);
|
||||
},
|
||||
|
||||
updateToLatest() {
|
||||
return ajax(this.diffLocalChangesUrl).then((json) => {
|
||||
if (json && json.error) {
|
||||
bootbox.alert(
|
||||
I18n.t("generic_error_with_reason", {
|
||||
error: json.error,
|
||||
})
|
||||
);
|
||||
} else if (json && json.diff) {
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.customize.theme.update_confirm") +
|
||||
`<pre><code class="diff">${escapeExpression(
|
||||
json.diff
|
||||
)}</code></pre>`,
|
||||
I18n.t("cancel"),
|
||||
I18n.t("admin.customize.theme.update_confirm_yes"),
|
||||
(result) => {
|
||||
if (result) {
|
||||
return this.save({ remote_update: true }).then(() =>
|
||||
this.set("changed", false)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
// TODO: Models shouldn't be updating the DOM
|
||||
highlightSyntax(undefined, this.siteSettings, this.session);
|
||||
} else {
|
||||
return this.save({ remote_update: true }).then(() =>
|
||||
this.set("changed", false)
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
changed: false,
|
||||
|
||||
saveChanges() {
|
||||
const hash = this.getProperties.apply(this, arguments);
|
||||
return this.save(hash)
|
||||
.finally(() => this.set("changed", false))
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
saveSettings(name, value) {
|
||||
const settings = {};
|
||||
settings[name] = value;
|
||||
return this.save({ settings });
|
||||
},
|
||||
|
||||
saveTranslation(name, value) {
|
||||
return this.save({ translations: { [name]: value } });
|
||||
},
|
||||
});
|
||||
|
||||
export default Theme;
|
||||
@@ -0,0 +1,75 @@
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
export default EmberObject.extend({
|
||||
@discourseComputed("days_visited", "time_period")
|
||||
days_visited_percent(daysVisited, timePeriod) {
|
||||
return Math.round((daysVisited * 100) / timePeriod);
|
||||
},
|
||||
|
||||
@discourseComputed("min_days_visited", "time_period")
|
||||
min_days_visited_percent(minDaysVisited, timePeriod) {
|
||||
return Math.round((minDaysVisited * 100) / timePeriod);
|
||||
},
|
||||
|
||||
@discourseComputed("num_topics_replied_to", "min_topics_replied_to")
|
||||
capped_topics_replied_to(numReplied, minReplied) {
|
||||
return numReplied > minReplied;
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"days_visited",
|
||||
"min_days_visited",
|
||||
"num_topics_replied_to",
|
||||
"min_topics_replied_to",
|
||||
"topics_viewed",
|
||||
"min_topics_viewed",
|
||||
"posts_read",
|
||||
"min_posts_read",
|
||||
"num_flagged_posts",
|
||||
"max_flagged_posts",
|
||||
"topics_viewed_all_time",
|
||||
"min_topics_viewed_all_time",
|
||||
"posts_read_all_time",
|
||||
"min_posts_read_all_time",
|
||||
"num_flagged_by_users",
|
||||
"max_flagged_by_users",
|
||||
"num_likes_given",
|
||||
"min_likes_given",
|
||||
"num_likes_received",
|
||||
"min_likes_received",
|
||||
"num_likes_received",
|
||||
"min_likes_received",
|
||||
"num_likes_received_days",
|
||||
"min_likes_received_days",
|
||||
"num_likes_received_users",
|
||||
"min_likes_received_users",
|
||||
"trust_level_locked",
|
||||
"penalty_counts.silenced",
|
||||
"penalty_counts.suspended"
|
||||
)
|
||||
met() {
|
||||
return {
|
||||
days_visited: this.days_visited >= this.min_days_visited,
|
||||
topics_replied_to:
|
||||
this.num_topics_replied_to >= this.min_topics_replied_to,
|
||||
topics_viewed: this.topics_viewed >= this.min_topics_viewed,
|
||||
posts_read: this.posts_read >= this.min_posts_read,
|
||||
topics_viewed_all_time:
|
||||
this.topics_viewed_all_time >= this.min_topics_viewed_all_time,
|
||||
posts_read_all_time:
|
||||
this.posts_read_all_time >= this.min_posts_read_all_time,
|
||||
flagged_posts: this.num_flagged_posts <= this.max_flagged_posts,
|
||||
flagged_by_users: this.num_flagged_by_users <= this.max_flagged_by_users,
|
||||
likes_given: this.num_likes_given >= this.min_likes_given,
|
||||
likes_received: this.num_likes_received >= this.min_likes_received,
|
||||
likes_received_days:
|
||||
this.num_likes_received_days >= this.min_likes_received_days,
|
||||
likes_received_users:
|
||||
this.num_likes_received_users >= this.min_likes_received_users,
|
||||
level_locked: this.trust_level_locked,
|
||||
silenced: this.get("penalty_counts.silenced") === 0,
|
||||
suspended: this.get("penalty_counts.suspended") === 0,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import EmberObject from "@ember/object";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import { i18n } from "discourse/lib/computed";
|
||||
|
||||
const UserField = RestModel.extend();
|
||||
|
||||
const UserFieldType = EmberObject.extend({
|
||||
name: i18n("id", "admin.user_fields.field_types.%@"),
|
||||
});
|
||||
|
||||
UserField.reopenClass({
|
||||
fieldTypes() {
|
||||
if (!this._fieldTypes) {
|
||||
this._fieldTypes = [
|
||||
UserFieldType.create({ id: "text" }),
|
||||
UserFieldType.create({ id: "confirm" }),
|
||||
UserFieldType.create({ id: "dropdown", hasOptions: true }),
|
||||
];
|
||||
}
|
||||
|
||||
return this._fieldTypes;
|
||||
},
|
||||
|
||||
fieldTypeById(id) {
|
||||
return this.fieldTypes().findBy("id", id);
|
||||
},
|
||||
});
|
||||
|
||||
export default UserField;
|
||||
@@ -0,0 +1,44 @@
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const VersionCheck = EmberObject.extend({
|
||||
@discourseComputed("updated_at")
|
||||
noCheckPerformed(updatedAt) {
|
||||
return updatedAt === null;
|
||||
},
|
||||
|
||||
@discourseComputed("missing_versions_count")
|
||||
upToDate(missingVersionsCount) {
|
||||
return missingVersionsCount === 0 || missingVersionsCount === null;
|
||||
},
|
||||
|
||||
@discourseComputed("missing_versions_count")
|
||||
behindByOneVersion(missingVersionsCount) {
|
||||
return missingVersionsCount === 1;
|
||||
},
|
||||
|
||||
@discourseComputed("installed_sha")
|
||||
gitLink(installedSHA) {
|
||||
if (installedSHA) {
|
||||
return `https://github.com/discourse/discourse/commits/${installedSHA}`;
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("installed_sha")
|
||||
shortSha(installedSHA) {
|
||||
if (installedSHA) {
|
||||
return installedSHA.substr(0, 10);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
VersionCheck.reopenClass({
|
||||
find() {
|
||||
return ajax("/admin/version_check").then((json) =>
|
||||
VersionCheck.create(json)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default VersionCheck;
|
||||
@@ -0,0 +1,55 @@
|
||||
import I18n from "I18n";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const WatchedWord = EmberObject.extend({
|
||||
save() {
|
||||
return ajax(
|
||||
"/admin/logs/watched_words" + (this.id ? "/" + this.id : "") + ".json",
|
||||
{
|
||||
type: this.id ? "PUT" : "POST",
|
||||
data: { word: this.word, action_key: this.action },
|
||||
dataType: "json",
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
destroy() {
|
||||
return ajax("/admin/logs/watched_words/" + this.id + ".json", {
|
||||
type: "DELETE",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
WatchedWord.reopenClass({
|
||||
findAll() {
|
||||
return ajax("/admin/logs/watched_words.json").then((list) => {
|
||||
const actions = {};
|
||||
list.words.forEach((s) => {
|
||||
if (!actions[s.action]) {
|
||||
actions[s.action] = [];
|
||||
}
|
||||
actions[s.action].pushObject(WatchedWord.create(s));
|
||||
});
|
||||
|
||||
list.actions.forEach((a) => {
|
||||
if (!actions[a]) {
|
||||
actions[a] = [];
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(actions).map((n) => {
|
||||
return EmberObject.create({
|
||||
nameKey: n,
|
||||
name: I18n.t("admin.watched_words.actions." + n),
|
||||
words: actions[n],
|
||||
count: actions[n].length,
|
||||
regularExpressions: list.regular_expressions,
|
||||
compiledRegularExpression: list.compiled_regular_expressions[n],
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default WatchedWord;
|
||||
@@ -0,0 +1,100 @@
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import Category from "discourse/models/category";
|
||||
import Group from "discourse/models/group";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import Site from "discourse/models/site";
|
||||
|
||||
export default RestModel.extend({
|
||||
content_type: 1, // json
|
||||
last_delivery_status: 1, // inactive
|
||||
wildcard_web_hook: false,
|
||||
verify_certificate: true,
|
||||
active: false,
|
||||
web_hook_event_types: null,
|
||||
groupsFilterInName: null,
|
||||
|
||||
@discourseComputed("wildcard_web_hook")
|
||||
webHookType: {
|
||||
get(wildcard) {
|
||||
return wildcard ? "wildcard" : "individual";
|
||||
},
|
||||
set(value) {
|
||||
this.set("wildcard_web_hook", value === "wildcard");
|
||||
},
|
||||
},
|
||||
|
||||
@discourseComputed("category_ids")
|
||||
categories(categoryIds) {
|
||||
return Category.findByIds(categoryIds);
|
||||
},
|
||||
|
||||
@observes("group_ids")
|
||||
updateGroupsFilter() {
|
||||
const groupIds = this.group_ids;
|
||||
this.set(
|
||||
"groupsFilterInName",
|
||||
Site.currentProp("groups").reduce((groupNames, g) => {
|
||||
if (groupIds.includes(g.id)) {
|
||||
groupNames.push(g.name);
|
||||
}
|
||||
return groupNames;
|
||||
}, [])
|
||||
);
|
||||
},
|
||||
|
||||
groupFinder(term) {
|
||||
return Group.findAll({ term: term, ignore_automatic: false });
|
||||
},
|
||||
|
||||
@discourseComputed("wildcard_web_hook", "web_hook_event_types.[]")
|
||||
description(isWildcardWebHook, types) {
|
||||
let desc = "";
|
||||
|
||||
types.forEach((type) => {
|
||||
const name = `${type.name.toLowerCase()}_event`;
|
||||
desc += desc !== "" ? `, ${name}` : name;
|
||||
});
|
||||
|
||||
return isWildcardWebHook ? "*" : desc;
|
||||
},
|
||||
|
||||
createProperties() {
|
||||
const types = this.web_hook_event_types;
|
||||
const categoryIds = this.categories.map((c) => c.id);
|
||||
const tagNames = this.tag_names;
|
||||
|
||||
// Hack as {{group-selector}} accepts a comma-separated string as data source, but
|
||||
// we use an array to populate the datasource above.
|
||||
const groupsFilter = this.groupsFilterInName;
|
||||
const groupNames =
|
||||
typeof groupsFilter === "string" ? groupsFilter.split(",") : groupsFilter;
|
||||
|
||||
return {
|
||||
payload_url: this.payload_url,
|
||||
content_type: this.content_type,
|
||||
secret: this.secret,
|
||||
wildcard_web_hook: this.wildcard_web_hook,
|
||||
verify_certificate: this.verify_certificate,
|
||||
active: this.active,
|
||||
web_hook_event_type_ids: isEmpty(types)
|
||||
? [null]
|
||||
: types.map((type) => type.id),
|
||||
category_ids: isEmpty(categoryIds) ? [null] : categoryIds,
|
||||
tag_names: isEmpty(tagNames) ? [null] : tagNames,
|
||||
group_ids:
|
||||
isEmpty(groupNames) || isEmpty(groupNames[0])
|
||||
? [null]
|
||||
: Site.currentProp("groups").reduce((groupIds, g) => {
|
||||
if (groupNames.includes(g.name)) {
|
||||
groupIds.push(g.id);
|
||||
}
|
||||
return groupIds;
|
||||
}, []),
|
||||
};
|
||||
},
|
||||
|
||||
updateProperties() {
|
||||
return this.createProperties();
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user