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:
parent
fc8fff88f6
commit
19672faba6
|
@ -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 {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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" });
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -129,6 +129,14 @@ export default {
|
|||
external: false,
|
||||
segment: "secondary",
|
||||
},
|
||||
{
|
||||
id: 338,
|
||||
name: "Invite",
|
||||
value: "/new-invite",
|
||||
icon: "paper-plane",
|
||||
external: false,
|
||||
segment: "primary",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NewInviteController < ApplicationController
|
||||
def index
|
||||
end
|
||||
end
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
Fabricate(:category_sidebar_section_link, linkable: category_sidebar_section_link.linkable)
|
||||
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link)
|
||||
category_sidebar_section_link_2 =
|
||||
Fabricate(:category_sidebar_section_link, linkable: category_sidebar_section_link.linkable)
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue