FEATURE: Add invite link to the sidebar (#29448)

This commit adds a new "Invite" link to the sidebar for all users who can invite to the site. Clicking the link opens the invite modal without changing the current route the user is on. Admins can customize the new link or remove it entirely if they wish by editing the sidebar section.

Internal topic: t/129752.
This commit is contained in:
Osama Sayegh 2024-10-30 05:31:14 +03:00 committed by GitHub
parent fc8fff88f6
commit 19672faba6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 219 additions and 11 deletions

View File

@ -13,6 +13,7 @@ import {
} from "discourse/lib/sidebar/custom-community-section-links";
import SectionLink from "discourse/lib/sidebar/section-link";
import AdminSectionLink from "discourse/lib/sidebar/user/community-section/admin-section-link";
import InviteSectionLink from "discourse/lib/sidebar/user/community-section/invite-section-link";
import MyPostsSectionLink from "discourse/lib/sidebar/user/community-section/my-posts-section-link";
import ReviewSectionLink from "discourse/lib/sidebar/user/community-section/review-section-link";
@ -26,6 +27,7 @@ const SPECIAL_LINKS_MAP = {
"/badges": BadgesSectionLink,
"/admin": AdminSectionLink,
"/g": GroupsSectionLink,
"/new-invite": InviteSectionLink,
};
export default class CommunitySection {

View File

@ -0,0 +1,31 @@
import BaseSectionLink from "discourse/lib/sidebar/base-community-section-link";
import I18n from "discourse-i18n";
export default class InviteSectionLink extends BaseSectionLink {
get name() {
return "invite";
}
get route() {
return "new-invite";
}
get title() {
return I18n.t("sidebar.sections.community.links.invite.content");
}
get text() {
return I18n.t(
`sidebar.sections.community.links.${this.overridenName.toLowerCase()}.content`,
{ defaultValue: this.overridenName }
);
}
get shouldDisplay() {
return !!this.currentUser?.can_invite_to_forum;
}
get defaultPrefixValue() {
return "paper-plane";
}
}

View File

@ -225,6 +225,7 @@ export default function () {
this.route("new-topic");
this.route("new-message");
this.route("new-invite");
this.route("badges", { resetNamespace: true }, function () {
this.route("show", { path: "/:id/:slug" });

View File

@ -0,0 +1,44 @@
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import CreateInvite from "discourse/components/modal/create-invite";
import cookie from "discourse/lib/cookie";
import DiscourseRoute from "discourse/routes/discourse";
import I18n from "discourse-i18n";
export default class extends DiscourseRoute {
@service router;
@service modal;
@service dialog;
@service currentUser;
async beforeModel(transition) {
if (this.currentUser) {
if (transition.from) {
// when navigating from another ember route
transition.abort();
this.#openInviteModalIfAllowed();
} else {
// when landing on this route from a full page load
this.router
.replaceWith("discovery.latest")
.followRedirects()
.then(() => {
this.#openInviteModalIfAllowed();
});
}
} else {
cookie("destination_url", window.location.href);
this.router.replaceWith("login");
}
}
#openInviteModalIfAllowed() {
next(() => {
if (this.currentUser.can_invite_to_forum) {
this.modal.show(CreateInvite, { model: { invites: [] } });
} else {
this.dialog.alert(I18n.t("user.invited.cannot_invite_to_forum"));
}
});
}
}

View File

@ -655,6 +655,30 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
);
});
test("the invite section link is not visible to people who cannot invite to the forum", async function (assert) {
updateCurrentUser({ can_invite_to_forum: false });
await visit("/");
assert
.dom(
".sidebar-section[data-section-name='community'] .sidebar-section-link[data-link-name='invite']"
)
.doesNotExist("invite section link is not visible");
});
test("clicking the invite section link opens the invite modal and doesn't change the route", async function (assert) {
updateCurrentUser({ can_invite_to_forum: true });
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-section-link[data-link-name='invite']"
);
assert.dom(".create-invite-modal").exists("invite modal is open");
assert.strictEqual(currentURL(), "/", "route doesn't change");
});
test("visiting top route", async function (assert) {
await visit("/top");

View File

@ -129,6 +129,14 @@ export default {
external: false,
segment: "secondary",
},
{
id: 338,
name: "Invite",
value: "/new-invite",
icon: "paper-plane",
external: false,
segment: "primary",
},
],
},
],

View File

@ -803,6 +803,14 @@ export default {
external: false,
segment: "secondary",
},
{
id: 338,
name: "Invite",
value: "/new-invite",
icon: "paper-plane",
external: false,
segment: "primary",
},
],
slug: "community",
public: true,

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class NewInviteController < ApplicationController
def index
end
end

View File

@ -21,6 +21,12 @@ class SidebarUrl < ActiveRecord::Base
},
{ name: "Review", path: "/review", icon: "flag", segment: SidebarUrl.segments["primary"] },
{ name: "Admin", path: "/admin", icon: "wrench", segment: SidebarUrl.segments["primary"] },
{
name: "Invite",
path: "/new-invite",
icon: "paper-plane",
segment: SidebarUrl.segments["primary"],
},
{ name: "Users", path: "/u", icon: "users", segment: SidebarUrl.segments["secondary"] },
{
name: "About",

View File

@ -1964,6 +1964,7 @@ en:
title: "Invite Link"
success: "Invite link generated successfully!"
error: "There was an error generating Invite link"
cannot_invite_to_forum: "Sorry, you don't have permission to create invites. Please contact an admin to grant you invite permission."
invite:
new_title: "Invite members"
@ -4945,6 +4946,9 @@ en:
pending_count:
one: "%{count} pending"
other: "%{count} pending"
invite:
content: "Invite"
title: "Invite new members"
global_section: "Global section, visible to everyone"
panels:
forum:

View File

@ -1351,6 +1351,7 @@ Discourse::Application.routes.draw do
get "new-topic" => "new_topic#index"
get "new-message" => "new_topic#index"
get "new-invite" => "new_invite#index"
# Topic routes
get "t/id_for/:slug" => "topics#id_for_slug"

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
class AddInvitesLinkToSidebar < ActiveRecord::Migration[7.1]
def up
community_section_id = DB.query_single(<<~SQL).first
SELECT id
FROM sidebar_sections
WHERE section_type = 0
SQL
return if !community_section_id
max_position = DB.query_single(<<~SQL, section_id: community_section_id).first
SELECT MAX(ssl.position)
FROM sidebar_urls su
JOIN sidebar_section_links ssl ON su.id = ssl.linkable_id
WHERE ssl.linkable_type = 'SidebarUrl'
AND ssl.sidebar_section_id = :section_id
AND su.segment = 0
SQL
updated_rows = DB.query_hash(<<~SQL, position: max_position, section_id: community_section_id)
DELETE FROM sidebar_section_links
WHERE position > :position
AND sidebar_section_id = :section_id
AND linkable_type = 'SidebarUrl'
RETURNING user_id, linkable_id, linkable_type, sidebar_section_id, position + 1 AS position, created_at, updated_at
SQL
updated_rows.each { |row| DB.exec(<<~SQL, **row.symbolize_keys) }
INSERT INTO sidebar_section_links
(user_id, linkable_id, linkable_type, sidebar_section_id, position, created_at, updated_at)
VALUES
(:user_id, :linkable_id, :linkable_type, :sidebar_section_id, :position, :created_at, :updated_at)
SQL
link_id = DB.query_single(<<~SQL).first
INSERT INTO sidebar_urls
(name, value, icon, external, segment, created_at, updated_at)
VALUES
('Invite', '/new-invite', 'paper-plane', false, 0, now(), now())
RETURNING sidebar_urls.id
SQL
DB.exec(<<~SQL, link_id:, section_id: community_section_id, position: max_position + 1)
INSERT INTO sidebar_section_links
(user_id, linkable_id, linkable_type, sidebar_section_id, position, created_at, updated_at)
VALUES
(-1, :link_id, 'SidebarUrl', :section_id, :position, now(), now())
SQL
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -47,13 +47,17 @@ RSpec.describe Category do
it "should delete associated sidebar_section_links when category is destroyed" do
category_sidebar_section_link = Fabricate(:category_sidebar_section_link)
category_sidebar_section_link_2 =
Fabricate(:category_sidebar_section_link, linkable: category_sidebar_section_link.linkable)
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link)
expect { category_sidebar_section_link.linkable.destroy! }.to change {
SidebarSectionLink.count
}.from(12).to(10)
expect(SidebarSectionLink.last).to eq(tag_sidebar_section_link)
expect(
SidebarSectionLink.where(
id: [category_sidebar_section_link.id, category_sidebar_section_link_2.id],
).count,
).to eq(0)
end
end

View File

@ -22,7 +22,18 @@ RSpec.describe SidebarSection do
expect(community_section.reload.title).to eq("Community")
expect(community_section.sidebar_section_links.all.map { |link| link.linkable.name }).to eq(
["Topics", "My Posts", "Review", "Admin", "Users", "About", "FAQ", "Groups", "Badges"],
[
"Topics",
"My Posts",
"Review",
"Admin",
"Invite",
"Users",
"About",
"FAQ",
"Groups",
"Badges",
],
)
end
end

View File

@ -25,12 +25,15 @@ RSpec.describe Tag do
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link)
tag_sidebar_section_link_2 =
Fabricate(:tag_sidebar_section_link, linkable: tag_sidebar_section_link.linkable)
category_sidebar_section_link = Fabricate(:category_sidebar_section_link)
expect { tag_sidebar_section_link.linkable.destroy! }.to change {
SidebarSectionLink.count
}.from(12).to(10)
expect(SidebarSectionLink.last).to eq(category_sidebar_section_link)
expect(
SidebarSectionLink.where(
id: [tag_sidebar_section_link.id, tag_sidebar_section_link_2.id],
).count,
).to eq(0)
end
end

View File

@ -23,7 +23,7 @@ RSpec.describe "Editing Sidebar Community Section", type: :system do
visit("/latest")
expect(sidebar.primary_section_icons("community")).to eq(
%w[layer-group user flag wrench ellipsis-vertical],
%w[layer-group user flag wrench paper-plane ellipsis-vertical],
)
modal = sidebar.click_community_section_more_button.click_customize_community_section_button
@ -33,11 +33,11 @@ RSpec.describe "Editing Sidebar Community Section", type: :system do
modal.confirm_update
expect(sidebar.primary_section_links("community")).to eq(
["My Posts", "Topics", "Review", "Admin", "More"],
["My Posts", "Topics", "Review", "Admin", "Invite", "More"],
)
expect(sidebar.primary_section_icons("community")).to eq(
%w[user paper-plane flag wrench ellipsis-vertical],
%w[user paper-plane flag wrench paper-plane ellipsis-vertical],
)
modal = sidebar.click_community_section_more_button.click_customize_community_section_button
@ -46,11 +46,11 @@ RSpec.describe "Editing Sidebar Community Section", type: :system do
expect(sidebar).to have_section("Community")
expect(sidebar.primary_section_links("community")).to eq(
["Topics", "My Posts", "Review", "Admin", "More"],
["Topics", "My Posts", "Review", "Admin", "Invite", "More"],
)
expect(sidebar.primary_section_icons("community")).to eq(
%w[layer-group user flag wrench ellipsis-vertical],
%w[layer-group user flag wrench paper-plane ellipsis-vertical],
)
end