discourse-assign/assets/javascripts/discourse/initializers/extend-for-assigns.js

917 lines
26 KiB
JavaScript

import { getOwner } from "@ember/application";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils";
import { hbs } from "ember-cli-htmlbars";
import { h } from "virtual-dom";
import { renderAvatar } from "discourse/helpers/user-avatar";
import discourseComputed from "discourse/lib/decorators";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import getURL from "discourse/lib/get-url";
import { iconHTML, iconNode } from "discourse/lib/icon-library";
import { withPluginApi } from "discourse/lib/plugin-api";
import { registerTopicFooterDropdown } from "discourse/lib/register-topic-footer-dropdown";
import { applyValueTransformer } from "discourse/lib/transformer";
import { escapeExpression } from "discourse/lib/utilities";
import RawHtml from "discourse/widgets/raw-html";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import { i18n } from "discourse-i18n";
import AssignButton, {
assignPost,
unassignPost,
} from "../components/assign-button";
import BulkActionsAssignUser from "../components/bulk-actions/bulk-assign-user";
import EditTopicAssignments from "../components/modal/edit-topic-assignments";
import TopicLevelAssignMenu from "../components/topic-level-assign-menu";
import { extendTopicModel } from "../models/topic";
const DEPENDENT_KEYS = [
"topic.assigned_to_user",
"topic.assigned_to_group",
"currentUser.can_assign",
"topic.assigned_to_user.username",
];
function defaultTitle(topic) {
const username =
topic.assigned_to_user?.username || topic.assigned_to_group?.name;
if (username) {
return i18n("discourse_assign.unassign.help", { username });
} else {
return i18n("discourse_assign.assign.help");
}
}
function registerTopicFooterButtons(api) {
registerTopicFooterDropdown(TopicLevelAssignMenu);
api.registerTopicFooterButton({
id: "assign",
icon() {
return this.topic.isAssigned()
? this.site.mobileView
? "user-xmark"
: null
: "user-plus";
},
priority: 250,
translatedTitle() {
return defaultTitle(this.topic);
},
translatedAriaLabel() {
return defaultTitle(this.topic);
},
translatedLabel() {
return i18n("discourse_assign.assign.title");
},
async action() {
if (!this.currentUser?.can_assign) {
return;
}
const taskActions = getOwner(this).lookup("service:task-actions");
const modal = getOwner(this).lookup("service:modal");
if (this.topic.isAssigned()) {
this.set("topic.assigned_to_user", null);
this.set("topic.assigned_to_group", null);
await taskActions.unassign(this.topic.id, "Topic");
this.appEvents.trigger("post-stream:refresh", {
id: this.topic.postStream.firstPostId,
});
} else {
await modal.show(EditTopicAssignments, {
model: {
topic: this.topic,
},
onSuccess: () =>
this.appEvents.trigger("post-stream:refresh", {
id: this.topic.postStream.firstPostId,
}),
});
}
},
dropdown() {
return this.site.mobileView;
},
classNames: ["assign"],
dependentKeys: DEPENDENT_KEYS,
displayed() {
return (
this.currentUser?.can_assign &&
!this.topic.isAssigned() &&
!this.topic.hasAssignedPosts()
);
},
});
api.registerTopicFooterButton({
id: "unassign-mobile-header",
translatedTitle() {
return defaultTitle(this.topic);
},
translatedAriaLabel() {
return defaultTitle(this.topic);
},
translatedLabel() {
const user = this.topic.assigned_to_user;
const group = this.topic.assigned_to_group;
const label = i18n("discourse_assign.assigned_to_w_ellipsis");
if (user) {
return htmlSafe(
`<span class="unassign-label"><span class="text">${label}</span><span class="username">${
user.username
}</span></span>${renderAvatar(user, {
imageSize: "small",
ignoreTitle: true,
})}`
);
} else if (group) {
return htmlSafe(
`<span class="unassign-label">${label}</span> @${group.name}`
);
}
},
dropdown() {
return this.currentUser?.can_assign && this.topic.isAssigned();
},
classNames: ["assign"],
dependentKeys: DEPENDENT_KEYS,
displayed() {
// only display the button in the mobile view
return this.currentUser?.can_assign && this.site.mobileView;
},
});
api.registerTopicFooterButton({
id: "unassign-mobile",
icon() {
return "user-xmark";
},
translatedTitle() {
return defaultTitle(this.topic);
},
translatedAriaLabel() {
return defaultTitle(this.topic);
},
translatedLabel() {
const label = i18n("discourse_assign.unassign.title");
return htmlSafe(
`<span class="unassign-label"><span class="text">${label}</span></span>`
);
},
action() {
if (!this.currentUser?.can_assign) {
return;
}
const taskActions = getOwner(this).lookup("service:task-actions");
this.set("topic.assigned_to_user", null);
this.set("topic.assigned_to_group", null);
taskActions.unassign(this.topic.id).then(() => {
this.appEvents.trigger("post-stream:refresh", {
id: this.topic.postStream.firstPostId,
});
});
},
dropdown() {
return this.currentUser?.can_assign && this.topic.isAssigned();
},
classNames: ["assign"],
dependentKeys: DEPENDENT_KEYS,
displayed() {
// only display the button in the mobile view
return (
this.currentUser?.can_assign &&
this.site.mobileView &&
this.topic.isAssigned()
);
},
});
api.registerTopicFooterButton({
id: "reassign-self-mobile",
icon() {
return "user-plus";
},
translatedTitle() {
return i18n("discourse_assign.reassign.to_self_help");
},
translatedAriaLabel() {
return i18n("discourse_assign.reassign.to_self_help");
},
translatedLabel() {
const label = i18n("discourse_assign.reassign.to_self");
return htmlSafe(
`<span class="unassign-label"><span class="text">${label}</span></span>`
);
},
async action() {
if (!this.currentUser?.can_assign) {
return;
}
const taskActions = getOwner(this).lookup("service:task-actions");
this.set("topic.assigned_to_user", null);
this.set("topic.assigned_to_group", null);
await taskActions.reassignUserToTopic(this.currentUser, this.topic);
this.appEvents.trigger("post-stream:refresh", {
id: this.topic.postStream.firstPostId,
});
},
dropdown() {
return this.currentUser?.can_assign && this.topic.isAssigned();
},
classNames: ["assign"],
dependentKeys: DEPENDENT_KEYS,
displayed() {
return (
// only display the button in the mobile view
this.site.mobileView &&
this.currentUser?.can_assign &&
this.topic.isAssigned() &&
this.topic.assigned_to_user?.username !== this.currentUser.username
);
},
});
api.registerTopicFooterButton({
id: "reassign-mobile",
icon() {
return "group-plus";
},
translatedTitle() {
return i18n("discourse_assign.reassign.help");
},
translatedAriaLabel() {
return i18n("discourse_assign.reassign.help");
},
translatedLabel() {
const label = i18n("discourse_assign.reassign.title_w_ellipsis");
return htmlSafe(
`<span class="unassign-label"><span class="text">${label}</span></span>`
);
},
async action() {
if (!this.currentUser?.can_assign) {
return;
}
const taskActions = getOwner(this).lookup("service:task-actions");
await taskActions.showAssignModal(this.topic, {
targetType: "Topic",
isAssigned: this.topic.isAssigned(),
onSuccess: () =>
this.appEvents.trigger("post-stream:refresh", {
id: this.topic.postStream.firstPostId,
}),
});
},
dropdown() {
return this.currentUser?.can_assign && this.topic.isAssigned();
},
classNames: ["assign"],
dependentKeys: DEPENDENT_KEYS,
displayed() {
// only display the button in the mobile view
return this.currentUser?.can_assign && this.site.mobileView;
},
});
}
function initialize(api) {
const siteSettings = api.container.lookup("service:site-settings");
const currentUser = api.getCurrentUser();
if (siteSettings.assigns_public || currentUser?.can_assign) {
api.addNavigationBarItem({
name: "unassigned",
customFilter: (category) => {
return category?.custom_fields?.enable_unassigned_filter === "true";
},
customHref: (category) => {
if (category) {
return getURL(category.url) + "/l/latest?status=open&assigned=nobody";
}
},
forceActive: (category, args) => {
const queryParams = args.currentRouteQueryParams;
return (
queryParams &&
Object.keys(queryParams).length === 2 &&
queryParams["assigned"] === "nobody" &&
queryParams["status"] === "open"
);
},
before: "top",
});
if (api.getCurrentUser()?.can_assign) {
customizePostMenu(api);
}
}
api.addPostSmallActionClassesCallback((post) => {
if (post.actionCode.includes("assigned") && !siteSettings.assigns_public) {
return ["private-assign"];
}
});
api.addAdvancedSearchOptions(
api.getCurrentUser()?.can_assign
? {
inOptionsForUsers: [
{
name: i18n("search.advanced.in.assigned"),
value: "assigned",
},
{
name: i18n("search.advanced.in.unassigned"),
value: "unassigned",
},
],
}
: {}
);
function assignedToUserPath(assignedToUser) {
return getURL(
siteSettings.assigns_user_url_path.replace(
"{username}",
assignedToUser.username
)
);
}
function assignedToGroupPath(assignedToGroup) {
return getURL(`/g/${assignedToGroup.name}/assigned/everyone`);
}
api.modifyClass(
"model:bookmark",
(Superclass) =>
class extends Superclass {
@discourseComputed("assigned_to_user")
assignedToUserPath(assignedToUser) {
return assignedToUserPath(assignedToUser);
}
@discourseComputed("assigned_to_group")
assignedToGroupPath(assignedToGroup) {
return assignedToGroupPath(assignedToGroup);
}
}
);
api.modifyClass(
"component:topic-notifications-button",
(Superclass) =>
class extends Superclass {
get reasonText() {
if (
this.currentUser.never_auto_track_topics &&
this.args.topic.get("assigned_to_user.username") ===
this.currentUser.username
) {
return i18n("notification_reason.user");
}
return super.reasonText;
}
}
);
api.addPostSmallActionIcon("assigned", "user-plus");
api.addPostSmallActionIcon("assigned_to_post", "user-plus");
api.addPostSmallActionIcon("assigned_group", "group-plus");
api.addPostSmallActionIcon("assigned_group_to_post", "group-plus");
api.addPostSmallActionIcon("unassigned", "user-xmark");
api.addPostSmallActionIcon("unassigned_group", "group-times");
api.addPostSmallActionIcon("unassigned_from_post", "user-xmark");
api.addPostSmallActionIcon("unassigned_group_from_post", "group-times");
api.includePostAttributes("assigned_to_user", "assigned_to_group");
api.addPostSmallActionIcon("reassigned", "user-plus");
api.addPostSmallActionIcon("reassigned_group", "group-plus");
api.addPostTransformCallback((transformed) => {
if (
[
"assigned",
"unassigned",
"reassigned",
"assigned_group",
"unassigned_group",
"reassigned_group",
"assigned_to_post",
"assigned_group_to_post",
"unassigned_from_post",
"unassigned_group_from_post",
"details_change",
"note_change",
"status_change",
].includes(transformed.actionCode)
) {
transformed.isSmallAction = true;
transformed.canEdit = true;
}
});
api.addDiscoveryQueryParam("assigned", { replace: true, refreshModel: true });
api.addTagsHtmlCallback((topic, params = {}) => {
const [assignedToUser, assignedToGroup, topicNote] = Object.values(
topic.getProperties(
"assigned_to_user",
"assigned_to_group",
"assignment_note",
"assignment_status"
)
);
const topicAssignee = {
assignee: assignedToUser || assignedToGroup,
note: topicNote,
};
let assignedToIndirectly;
if (topic.indirectly_assigned_to) {
assignedToIndirectly = Object.entries(topic.indirectly_assigned_to).map(
([key, value]) => {
value.assigned_to.assignedToPostId = key;
return value;
}
);
} else {
assignedToIndirectly = [];
}
const assignedTo = []
.concat(
topicAssignee,
assignedToIndirectly.map((assigned) => ({
assignee: assigned.assigned_to,
note: assigned.assignment_note,
}))
)
.filter(({ assignee }) => assignee)
.flat();
if (!assignedTo) {
return "";
}
const createTagHtml = ({ assignee, note }) => {
let assignedPath;
if (assignee.assignedToPostId) {
assignedPath = `/p/${assignee.assignedToPostId}`;
} else {
assignedPath = `/t/${topic.id}`;
}
const icon = iconHTML(assignee.username ? "user-plus" : "group-plus");
const name = assignee.username || assignee.name;
const tagName = params.tagName || "a";
const href =
tagName === "a"
? `href="${getURL(assignedPath)}" data-auto-route="true"`
: "";
return `<${tagName} class="assigned-to discourse-tag simple" ${href}>${icon}<span title="${escapeExpression(
note
)}">${name}</span></${tagName}>`;
};
// is there's one assignment just return the tag
if (assignedTo.length === 1) {
return createTagHtml(assignedTo[0]);
}
// join multiple assignments with a separator
let result = "";
assignedTo.forEach((assignment, index) => {
result += createTagHtml(assignment);
// add separator if not the last tag
if (index < assignedTo.length - 1) {
const separator = applyValueTransformer("tag-separator", ",", {
topic,
index,
});
result += `<span class="discourse-tags__tag-separator">${separator}</span>`;
}
});
return result;
});
api.createWidget("assigned-to-post", {
html(attrs) {
return new RenderGlimmer(
this,
"p.assigned-to",
hbs`
<AssignedToPost @assignedToUser={{@data.assignedToUser}} @assignedToGroup={{@data.assignedToGroup}}
@href={{@data.href}} @post={{@data.post}} />`,
{
assignedToUser: attrs.post.assigned_to_user,
assignedToGroup: attrs.post.assigned_to_group,
href: attrs.href,
post: attrs.post,
}
);
},
});
api.createWidget("assigned-to-first-post", {
html(attrs) {
const topic = attrs.topic;
const [assignedToUser, assignedToGroup, indirectlyAssignedTo] = [
topic.assigned_to_user,
topic.assigned_to_group,
topic.indirectly_assigned_to,
];
const assigneeElements = [];
const assignedHtml = (username, path, type) => {
return `<span class="assigned-to--${type}">${htmlSafe(
i18n("discourse_assign.assigned_topic_to", {
username,
path,
})
)}</span>`;
};
if (assignedToUser) {
assigneeElements.push(
h(
"span.assignee",
new RawHtml({
html: assignedHtml(
assignedToUser.username,
assignedToUserPath(assignedToUser),
"user"
),
})
)
);
}
if (assignedToGroup) {
assigneeElements.push(
h(
"span.assignee",
new RawHtml({
html: assignedHtml(
assignedToGroup.name,
assignedToGroupPath(assignedToGroup),
"group"
),
})
)
);
}
if (indirectlyAssignedTo) {
Object.keys(indirectlyAssignedTo).map((postId) => {
const assignee = indirectlyAssignedTo[postId].assigned_to;
const postNumber = indirectlyAssignedTo[postId].post_number;
assigneeElements.push(
h("span.assignee", [
h(
"a",
{
attributes: {
class: "assigned-indirectly",
href: `${topic.url}/${postNumber}`,
},
},
i18n("discourse_assign.assign_post_to_multiple", {
post_number: postNumber,
username: assignee.username || assignee.name,
})
),
])
);
});
}
if (!isEmpty(assigneeElements)) {
return h("p.assigned-to", [
assignedToUser ? iconNode("user-plus") : iconNode("group-plus"),
assignedToUser || assignedToGroup
? ""
: h("span.assign-text", i18n("discourse_assign.assigned")),
assigneeElements,
]);
}
},
});
api.modifyClass(
"model:group",
(Superclass) =>
class extends Superclass {
asJSON() {
return Object.assign({}, super.asJSON(...arguments), {
assignable_level: this.assignable_level,
});
}
}
);
api.modifyClass(
"controller:topic",
(Superclass) =>
class extends Superclass {
subscribe() {
super.subscribe(...arguments);
this.messageBus.subscribe("/staff/topic-assignment", (data) => {
const topic = this.model;
const topicId = topic.id;
if (data.topic_id === topicId) {
let post;
if (data.post_id) {
post = topic.postStream.posts.find(
(p) => p.id === data.post_id
);
}
const target = post || topic;
target.set("assignment_note", data.assignment_note);
target.set("assignment_status", data.assignment_status);
if (data.assigned_type === "User") {
target.set(
"assigned_to_user_id",
data.type === "assigned" ? data.assigned_to.id : null
);
target.set("assigned_to_user", data.assigned_to);
}
if (data.assigned_type === "Group") {
target.set(
"assigned_to_group_id",
data.type === "assigned" ? data.assigned_to.id : null
);
target.set("assigned_to_group", data.assigned_to);
}
if (data.post_id) {
if (data.type === "unassigned") {
delete topic.indirectly_assigned_to[data.post_number];
}
this.appEvents.trigger("post-stream:refresh", {
id: topic.postStream.posts[0].id,
});
this.appEvents.trigger("post-stream:refresh", {
id: data.post_id,
});
}
if (topic.closed) {
this.appEvents.trigger("post-stream:refresh", {
id: topic.postStream.posts[0].id,
});
}
}
this.appEvents.trigger("header:update-topic", topic);
this.appEvents.trigger("post-stream:refresh", {
id: topic.postStream.posts[0].id,
});
});
}
unsubscribe() {
super.unsubscribe(...arguments);
if (!this.model?.id) {
return;
}
this.messageBus.unsubscribe("/staff/topic-assignment");
}
}
);
api.decorateWidget("post-contents:after-cooked", (dec) => {
const postModel = dec.getModel();
if (postModel) {
let assignedToUser, assignedToGroup, postAssignment, href;
if (dec.attrs.post_number === 1) {
return dec.widget.attach("assigned-to-first-post", {
topic: postModel.topic,
});
} else {
postAssignment =
postModel.topic.indirectly_assigned_to?.[postModel.id]?.assigned_to;
if (postAssignment?.username) {
assignedToUser = postAssignment;
}
if (postAssignment?.name) {
assignedToGroup = postAssignment;
}
}
if (assignedToUser || assignedToGroup) {
href = assignedToUser
? assignedToUserPath(assignedToUser)
: assignedToGroupPath(assignedToGroup);
}
if (href) {
return dec.widget.attach("assigned-to-post", {
assignedToUser,
assignedToGroup,
href,
post: postModel,
});
}
}
});
api.replaceIcon("notification.assigned", "user-plus");
api.replaceIcon(
"notification.discourse_assign.assign_group_notification",
"group-plus"
);
api.modifyClass(
"controller:preferences/notifications",
(Superclass) =>
class extends Superclass {
@action
save() {
this.saveAttrNames.push("custom_fields");
super.save(...arguments);
}
}
);
api.addKeyboardShortcut("g a", "", { path: "/my/activity/assigned" });
}
function customizePostMenu(api) {
const transformerRegistered = api.registerValueTransformer(
"post-menu-buttons",
({
value: dag,
context: {
post,
state,
firstButtonKey,
lastHiddenButtonKey,
secondLastHiddenButtonKey,
},
}) => {
dag.add(
"assign",
AssignButton,
post.assigned_to_user?.id === state.currentUser.id
? {
before: firstButtonKey,
}
: {
before: lastHiddenButtonKey,
after: secondLastHiddenButtonKey,
}
);
}
);
const silencedKey =
transformerRegistered && "discourse.post-menu-widget-overrides";
withSilencedDeprecations(silencedKey, () => customizeWidgetPostMenu(api));
}
function customizeWidgetPostMenu(api) {
api.addPostMenuButton("assign", (post) => {
if (post.firstPost) {
return;
}
if (post.assigned_to_user || post.assigned_to_group) {
return {
action: "unassignPost",
icon: "user-xmark",
className: "unassign-post",
title: "discourse_assign.unassign_post.title",
position:
post.assigned_to_user?.id === api.getCurrentUser().id
? "first"
: "second-last-hidden",
};
} else {
return {
action: "assignPost",
icon: "user-plus",
className: "assign-post",
title: "discourse_assign.assign_post.title",
position: "second-last-hidden",
};
}
});
api.attachWidgetAction("post", "assignPost", function () {
assignPost(this.model, getOwner(this).lookup("service:task-actions"));
});
api.attachWidgetAction("post", "unassignPost", function () {
unassignPost(this.model, getOwner(this).lookup("service:task-actions"));
});
}
const REGEXP_USERNAME_PREFIX = /^(assigned:)/gi;
export default {
name: "extend-for-assign",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
if (!siteSettings.assign_enabled) {
return;
}
withPluginApi("1.34.0", (api) => {
const currentUser = container.lookup("service:current-user");
if (currentUser?.can_assign) {
api.modifyClass(
"component:search-advanced-options",
(Superclass) =>
class extends Superclass {
updateSearchTermForAssignedUsername() {
const match = this.filterBlocks(REGEXP_USERNAME_PREFIX);
const userFilter = this.searchedTerms?.assigned;
let searchTerm = this.searchTerm || "";
let keyword = "assigned";
if (userFilter?.length !== 0) {
if (match.length !== 0) {
searchTerm = searchTerm.replace(
match[0],
`${keyword}:${userFilter}`
);
} else {
searchTerm += ` ${keyword}:${userFilter}`;
}
this._updateSearchTerm(searchTerm);
} else if (match.length !== 0) {
searchTerm = searchTerm.replace(match[0], "");
this._updateSearchTerm(searchTerm);
}
}
}
);
}
extendTopicModel(api);
initialize(api);
registerTopicFooterButtons(api);
api.addSearchSuggestion("in:assigned");
api.addSearchSuggestion("in:unassigned");
api.addGroupPostSmallActionCode("assigned_group");
api.addGroupPostSmallActionCode("reassigned_group");
api.addGroupPostSmallActionCode("unassigned_group");
api.addGroupPostSmallActionCode("assigned_group_to_post");
api.addGroupPostSmallActionCode("unassigned_group_from_post");
api.addUserSearchOption("assignableGroups");
api.addSaveableUserOptionField("notification_level_when_assigned");
api.addBulkActionButton({
id: "assign-topics",
label: "topics.bulk.assign",
icon: "user-plus",
class: "btn-default assign-topics",
action({ setComponent }) {
setComponent(BulkActionsAssignUser);
},
actionType: "setComponent",
});
api.addBulkActionButton({
id: "unassign-topics",
label: "topics.bulk.unassign",
icon: "user-xmark",
class: "btn-default unassign-topics",
action({ performAndRefresh }) {
performAndRefresh({ type: "unassign" });
},
actionType: "performAndRefresh",
});
});
},
};