From 6228f42ee636f0ce328632f9cbf1d6a2d051563b Mon Sep 17 00:00:00 2001 From: Bannon Tanner Date: Thu, 19 Mar 2026 14:10:52 -0500 Subject: [PATCH] FEAT: add group level gating (#66) Adds ability to gate topics by group membership. Shows separate subheading text and has input for a custom CTA button link. Group gating can work independently of the category and tag gating, or in conjunction. --- common/common.scss | 4 + .../components/topic-in-gated-category.gjs | 96 ++++++++---- locales/en.yml | 2 + settings.yml | 9 ++ spec/system/gated_topics_with_groups_spec.rb | 101 +++++++++++++ .../page_objects/components/gated_topic.rb | 20 +++ test/acceptance/gated-category-test.js | 143 ++++++++++++++++++ 7 files changed, 345 insertions(+), 30 deletions(-) create mode 100644 spec/system/gated_topics_with_groups_spec.rb diff --git a/common/common.scss b/common/common.scss index 3201b6a..70bda58 100644 --- a/common/common.scss +++ b/common/common.scss @@ -38,6 +38,10 @@ color: var(--primary-high); } } + + &__group { + margin-bottom: 1rem; + } } } diff --git a/javascripts/discourse/components/topic-in-gated-category.gjs b/javascripts/discourse/components/topic-in-gated-category.gjs index 25bbc5d..472811e 100644 --- a/javascripts/discourse/components/topic-in-gated-category.gjs +++ b/javascripts/discourse/components/topic-in-gated-category.gjs @@ -14,6 +14,10 @@ export default class TopicInGatedCategory extends Component { .map((id) => parseInt(id, 10)) .filter((id) => id); enabledTags = settings.enabled_tags.split("|").filter(Boolean); + enabledGroups = settings.enabled_groups + .split("|") + .map((id) => parseInt(id, 10)) + .filter((id) => !isNaN(id)); didInsertElement() { super.didInsertElement(...arguments); @@ -31,29 +35,41 @@ export default class TopicInGatedCategory extends Component { } recalculate() { - // do nothing if: - // a) topic does not have a category and does not have a gated tag - // b) component setting is empty - // c) user is logged in // TODO(https://github.com/discourse/discourse/pull/36678): The string check can be // removed using .discourse-compatibility once the PR is merged. - const gatedByTag = this.tags?.some((t) => { - const name = typeof t === "string" ? t : t.name; - return this.enabledTags.includes(name); - }); + // user is in an enabled group — always bypass if ( - (!this.categoryId && !gatedByTag) || - (this.enabledCategories.length === 0 && this.enabledTags.length === 0) || - this.currentUser + this.currentUser?.groups?.some((g) => this.enabledGroups.includes(g.id)) ) { return; } - if (this.enabledCategories.includes(this.categoryId) || gatedByTag) { - document.body.classList.add("topic-in-gated-category"); - this.set("hidden", false); + const hasGroupGating = this.enabledGroups.length > 0; + const gatedByCategory = this.enabledCategories.includes(this.categoryId); + const gatedByTag = this.tags?.some((t) => { + const name = typeof t === "string" ? t : t.name; + return this.enabledTags.includes(name); + }); + const hasAnyCategoryOrTag = + this.enabledCategories.length > 0 || this.enabledTags.length > 0; + + if (!hasAnyCategoryOrTag && !hasGroupGating) { + return; } + + // when categories/tags are configured, topic must match one + if (hasAnyCategoryOrTag && !gatedByCategory && !gatedByTag) { + return; + } + + // no groups configured — original behavior: any logged-in user bypasses + if (!hasGroupGating && this.currentUser) { + return; + } + + document.body.classList.add("topic-in-gated-category"); + this.set("hidden", false); } @computed("hidden") @@ -61,6 +77,10 @@ export default class TopicInGatedCategory extends Component { return !this.hidden; } + get showGroupGate() { + return this.currentUser && this.enabledGroups.length > 0; + } +