Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions javascripts/discourse/components/categories-groups.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export default class CategoriesGroups extends Component {
get categoryGroupList() {
const parsedSettings = settings.category_groups;
const extraLinks = settings.extra_links || [];
const trimmedName = (name) => (name || "").trim();

// Keep track of which categories landed in a group so the rest fall through
// to the "ungrouped" section.
Expand Down Expand Up @@ -98,6 +99,23 @@ export default class CategoriesGroups extends Component {

const items = withLinks(groupCategories);

// Links assigned to this group via `show_in_group` (matched against the
// default group name) go at the end, after the categories. This also
// lets a group contain only links and no categories.
const placedIds = new Set(
items.filter((i) => i.isExtraLink).map((i) => i.id)
);
extraLinks
.filter((link) => {
const target = trimmedName(link.show_in_group);
return (
target &&
target === trimmedName(obj.name) &&
!placedIds.has(link.id)
);
})
.forEach((link) => items.push(new ExtraLink(link)));

if (items.length > 0) {
// `slug` is derived from the default name so it stays stable across
// locales (used for CSS classes and localStorage collapse state),
Expand All @@ -121,11 +139,30 @@ export default class CategoriesGroups extends Component {
? this.categories.filter((c) => c.notification_level === 0)
: this.categories.filter((c) => c.hasMuted);

if (settings.show_ungrouped && ungroupedCategories.length > 0) {
// Links whose show_in_group matches no group name (typo or a renamed
// group) fall back to the ungrouped section instead of disappearing.
// Links with a show_before placement are left to that mechanism.
const groupNames = new Set(parsedSettings.map((g) => trimmedName(g.name)));
const orphanedLinks = extraLinks.filter((link) => {
const target = trimmedName(link.show_in_group);
return (
target &&
!groupNames.has(target) &&
(link.show_before || []).length === 0
);
});

if (
settings.show_ungrouped &&
(ungroupedCategories.length > 0 || orphanedLinks.length > 0)
) {
categoryGroupList.push({
name: i18n(themePrefix("ungrouped_categories_title")),
slug: "ungrouped",
items: withLinks(ungroupedCategories),
items: [
...withLinks(ungroupedCategories),
...orphanedLinks.map((link) => new ExtraLink(link)),
],
});
}

Expand Down
4 changes: 3 additions & 1 deletion locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ en:
settings:
category_groups: "Define each group with a name and the categories it contains (in display order). Use the translations field to localize a group's name per locale (e.g. locale 'fr')."
extra_links:
description: "Extra links that can be mixed into the category list. Set a link's \"show after\" category to position it after that category."
description: "Extra links that can be mixed into the category list. Use \"show before\" to position a link before a category, or \"show in group\" to add it to the end of a group."
schema:
properties:
id:
Expand All @@ -13,6 +13,8 @@ en:
description: "Use hex format, e.g. #00ff00"
show_before:
description: "The extra link will appear before this category."
show_in_group:
description: "The extra link will appear at the end of the group with this name (use the group's default, untranslated name). Allows a group to contain only links. If no group matches, the link falls back to the ungrouped section."
show_on_mobile: "Show the collapsible category box groups on mobile"
show_ungrouped: "Display a group of categories that aren't assigned to another group"
hide_muted_subcategories: "When enabled, a non-muted parent category will not appear under the muted section if it has a muted subcategory"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export default function migrate(settings, helpers) {
}
});

// Links with no category after them (links-only groups or trailing
// links) keep their group membership via `show_in_group`.
pendingLinks.forEach((link) => (link.show_in_group = name));

return { name, categories };
})
.filter(Boolean);
Expand Down
2 changes: 2 additions & 0 deletions settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ extra_links:
type: categories
validations:
max: 1
show_in_group:
type: string

show_on_mobile:
default: true
Expand Down
51 changes: 51 additions & 0 deletions spec/system/custom_category_group_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,57 @@
)
end

it "displays a group that contains only extra links" do
theme_component.update_setting(
:extra_links,
[
{
"id" => "links-only-link",
"url" => "https://meta.discourse.org",
"color" => "#0000ff",
"title" => "Community",
"show_in_group" => "Just Links",
},
].to_json,
)
theme_component.update_setting(
:category_groups,
[
{ "name" => "Default Categories", "categories" => [category.id] },
{ "name" => "Just Links", "categories" => [] },
].to_json,
)
theme_component.save!

visit "/categories"

within(".custom-category-group-just-links") do
expect(page).to have_css(".extra-link-links-only-link", text: "Community")
end
end

it "falls back to the ungrouped section when show_in_group matches no group" do
theme_component.update_setting(
:extra_links,
[
{
"id" => "orphaned-link",
"url" => "https://meta.discourse.org",
"color" => "#00ff00",
"title" => "Orphaned",
"show_in_group" => "Renamed Group",
},
].to_json,
)
theme_component.save!

visit "/categories"

within(".custom-category-group-ungrouped") do
expect(page).to have_css(".extra-link-orphaned-link", text: "Orphaned")
end
end

it "works with core category icons and emojis" do
category.update!(style_type: "emoji", emoji: "wave")
visit "/categories"
Expand Down