From 1b8b2a3a3227c919a6d11c92ba31691488af0949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Fri, 28 Mar 2025 19:31:33 -0300 Subject: [PATCH] DEV: Add compatibility with the Glimmer Post Stream --- .../components/assigned-to-first-post.gjs | 95 +++++++ .../discourse/components/assigned-to-post.gjs | 11 +- .../components/post-assignments-display.gjs | 49 ++++ .../initializers/extend-for-assigns.js | 235 ++++++++++++++---- assets/javascripts/discourse/lib/url.js | 19 ++ 5 files changed, 345 insertions(+), 64 deletions(-) create mode 100644 assets/javascripts/discourse/components/assigned-to-first-post.gjs create mode 100644 assets/javascripts/discourse/components/post-assignments-display.gjs create mode 100644 assets/javascripts/discourse/lib/url.js diff --git a/assets/javascripts/discourse/components/assigned-to-first-post.gjs b/assets/javascripts/discourse/components/assigned-to-first-post.gjs new file mode 100644 index 0000000..d1a292d --- /dev/null +++ b/assets/javascripts/discourse/components/assigned-to-first-post.gjs @@ -0,0 +1,95 @@ +import Component from "@glimmer/component"; +import { concat } from "@ember/helper"; +import icon from "discourse/helpers/d-icon"; +import userPrioritizedName from "discourse/helpers/user-prioritized-name"; +import { i18n } from "discourse-i18n"; +import { assignedToGroupPath, assignedToUserPath } from "../lib/url"; + +export default class AssignedToFirstPost extends Component { + get assignedToUser() { + return this.args.post?.topic?.assigned_to_user; + } + + get assignedToGroup() { + return this.args.post?.topic?.assigned_to_group; + } + + get icon() { + return this.assignedToUser ? "user-plus" : "group-plus"; + } + + get indirectlyAssignedTo() { + return this.args.post?.topic?.indirectly_assigned_to; + } + + get indirectAssignments() { + if (!this.indirectlyAssignedTo) { + return null; + } + + return Object.keys(this.indirectlyAssignedTo).map((postId) => { + const postNumber = this.indirectlyAssignedTo[postId].post_number; + + return { + postId, + assignee: this.indirectlyAssignedTo[postId].assigned_to, + postNumber, + url: `${this.args.post.topic.url}/${postNumber}`, + }; + }); + } + + get isAssigned() { + return !!( + this.assignedToUser || + this.assignedToGroup || + this.args.post?.topic?.indirectly_assigned_to + ); + } + + +} diff --git a/assets/javascripts/discourse/components/assigned-to-post.gjs b/assets/javascripts/discourse/components/assigned-to-post.gjs index 6c17144..80800a6 100644 --- a/assets/javascripts/discourse/components/assigned-to-post.gjs +++ b/assets/javascripts/discourse/components/assigned-to-post.gjs @@ -4,6 +4,7 @@ import { service } from "@ember/service"; import DButton from "discourse/components/d-button"; import DropdownMenu from "discourse/components/dropdown-menu"; import icon from "discourse/helpers/d-icon"; +import userPrioritizedName from "discourse/helpers/user-prioritized-name"; import { i18n } from "discourse-i18n"; import DMenu from "float-kit/components/d-menu"; @@ -11,14 +12,6 @@ export default class AssignedToPost extends Component { @service taskActions; @service siteSettings; - get nameOrUsername() { - if (this.siteSettings.prioritize_full_name_in_ux) { - return this.args.assignedToUser.name || this.args.assignedToUser.username; - } else { - return this.args.assignedToUser.username; - } - } - @action unassign() { this.taskActions.unassignPost(this.args.post); @@ -42,7 +35,7 @@ export default class AssignedToPost extends Component { {{#if @assignedToUser}} - {{this.nameOrUsername}} + {{userPrioritizedName @assignedToUser}} {{else}} {{@assignedToGroup.name}} {{/if}} diff --git a/assets/javascripts/discourse/components/post-assignments-display.gjs b/assets/javascripts/discourse/components/post-assignments-display.gjs new file mode 100644 index 0000000..c078c89 --- /dev/null +++ b/assets/javascripts/discourse/components/post-assignments-display.gjs @@ -0,0 +1,49 @@ +import Component from "@glimmer/component"; +import { assignedToGroupPath, assignedToUserPath } from "../lib/url"; +import AssignedFirstPost from "./assigned-to-first-post"; +import AssignedToPost from "./assigned-to-post"; + +export default class PostAssignmentsDisplay extends Component { + static shouldRender(args) { + return args.post; + } + + get post() { + return this.args.outletArgs.post; + } + + get assignedTo() { + return this.post.topic?.indirectly_assigned_to?.[this.post.id]?.assigned_to; + } + + get assignedToUser() { + return this.assignedTo.username ? this.assignedTo : null; + } + + get assignedToGroup() { + return !this.assignedToUser && this.assignedTo.name + ? this.assignedTo + : null; + } + + get assignedHref() { + return this.assignedToUser + ? assignedToUserPath(this.assignedToUser) + : assignedToGroupPath(this.assignedToGroup); + } + + +} diff --git a/assets/javascripts/discourse/initializers/extend-for-assigns.js b/assets/javascripts/discourse/initializers/extend-for-assigns.js index 96cfbd8..9cf2521 100644 --- a/assets/javascripts/discourse/initializers/extend-for-assigns.js +++ b/assets/javascripts/discourse/initializers/extend-for-assigns.js @@ -6,6 +6,7 @@ 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"; @@ -18,7 +19,9 @@ import { i18n } from "discourse-i18n"; import AssignButton from "../components/assign-button"; import BulkActionsAssignUser from "../components/bulk-actions/bulk-assign-user"; import EditTopicAssignments from "../components/modal/edit-topic-assignments"; +import PostAssignmentsDisplay from "../components/post-assignments-display"; import TopicLevelAssignMenu from "../components/topic-level-assign-menu"; +import { assignedToGroupPath, assignedToUserPath } from "../lib/url"; import { extendTopicModel } from "../models/topic"; const DEPENDENT_KEYS = [ @@ -322,7 +325,10 @@ function initialize(api) { } api.addPostSmallActionClassesCallback((post) => { - if (post.actionCode.includes("assigned") && !siteSettings.assigns_public) { + // TODO (glimmer-post-stream): only check for .action_code once the widget code is removed + const actionCode = post.action_code || post.actionCode; + + if (actionCode.includes("assigned") && !siteSettings.assigns_public) { return ["private-assign"]; } }); @@ -344,19 +350,6 @@ function initialize(api) { : {} ); - 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) => @@ -403,29 +396,6 @@ function initialize(api) { 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 = {}) => { @@ -717,6 +687,42 @@ function initialize(api) { } ); + customizePost(api); + + 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 customizePost(api) { + api.renderAfterWrapperOutlet( + "post-content-cooked-html", + PostAssignmentsDisplay + ); + + withSilencedDeprecations("discourse.post-stream-widget-overrides", () => + customizeWidgetPost(api) + ); +} + +function customizeWidgetPost(api) { api.decorateWidget("post-contents:after-cooked", (dec) => { const postModel = dec.getModel(); if (postModel) { @@ -752,26 +758,145 @@ function initialize(api) { } }); - 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.createWidget("assigned-to-post", { + html(attrs) { + return new RenderGlimmer( + this, + "p.assigned-to", + hbs` + `, + { + assignedToUser: attrs.post.assigned_to_user, + assignedToGroup: attrs.post.assigned_to_group, + href: attrs.href, + post: attrs.post, } - } - ); + ); + }, + }); - api.addKeyboardShortcut("g a", "", { path: "/my/activity/assigned" }); + 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 `${htmlSafe( + i18n("discourse_assign.assigned_topic_to", { + username, + path, + }) + )}`; + }; + + let displayedName = ""; + if (assignedToUser) { + displayedName = !this.siteSettings.prioritize_username_in_ux + ? assignedToUser.name || assignedToUser.username + : assignedToUser.username; + + assigneeElements.push( + h( + "span.assignee", + new RawHtml({ + html: assignedHtml( + displayedName, + 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; + + displayedName = + !this.siteSettings.prioritize_username_in_ux || !assignee.username + ? assignee.name || assignee.username + : assignee.username; + + 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: displayedName, + }) + ), + ]) + ); + }); + } + + 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, + ]); + } + }, + }); + + // This won't have a direct translation in the Glimmer API as it uses can_edit from the model + // TODO (glimmer-post-stream): check the post small action component and introduce a transformer to override the + // canEdit behavior there + 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; + } + }); } function customizePostMenu(api) { diff --git a/assets/javascripts/discourse/lib/url.js b/assets/javascripts/discourse/lib/url.js new file mode 100644 index 0000000..208cecf --- /dev/null +++ b/assets/javascripts/discourse/lib/url.js @@ -0,0 +1,19 @@ +import { getOwnerWithFallback } from "discourse/lib/get-owner"; +import getURL from "discourse/lib/get-url"; + +export function assignedToUserPath(assignedToUser) { + const siteSettings = getOwnerWithFallback(this).lookup( + "service:site-settings" + ); + + return getURL( + siteSettings.assigns_user_url_path.replace( + "{username}", + assignedToUser.username + ) + ); +} + +export function assignedToGroupPath(assignedToGroup) { + return getURL(`/g/${assignedToGroup.name}/assigned/everyone`); +}