WIP: Additional simplified category improvements (#37338)

* Change the simplified category upcoming change to Experimental so
admins can try it out
* Delete unused preview-related code for the category UI
* Preserve category permissions when switching between public & private
access toggle

<img width="329" height="245" alt="image"
src="https://github.com/user-attachments/assets/729ba0f9-5e1c-4e71-8b4f-8c26d59bae45"
/>

---------

Co-authored-by: Régis Hanol <regis@hanol.fr>
This commit is contained in:
Martin Brennan
2026-01-29 09:44:32 +10:00
committed by GitHub
co-authored by Régis Hanol
parent ce66d301ed
commit d9805c8635
7 changed files with 45 additions and 94 deletions
+1 -1
View File
@@ -4236,5 +4236,5 @@ experimental:
hidden: true
client: true
upcoming_change:
status: "conceptual"
status: "experimental"
impact: "feature,staff"
@@ -11,8 +11,6 @@ import categoryBadge from "discourse/helpers/category-badge";
import icon from "discourse/helpers/d-icon";
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
import { AUTO_GROUPS, CATEGORY_TEXT_COLORS } from "discourse/lib/constants";
import discourseDebounce from "discourse/lib/debounce";
import { INPUT_DELAY } from "discourse/lib/environment";
import getURL from "discourse/lib/get-url";
import Category from "discourse/models/category";
import PermissionType from "discourse/models/permission-type";
@@ -28,13 +26,13 @@ export default class UpsertCategoryGeneral extends Component {
@tracked categoryVisibilityState = null;
@tracked userModifiedPermissions = false;
uncategorizedSiteSettingLink = getURL(
"/admin/site_settings/category/all_results?filter=allow_uncategorized_topics"
);
customizeTextContentLink = getURL(
"/admin/customize/site_texts?q=uncategorized"
);
#previousPermissions = null;
get parentIsRestricted() {
const parentId = this.args.transientData.parent_category_id;
@@ -71,15 +69,27 @@ export default class UpsertCategoryGeneral extends Component {
}
set isPrivateCategory(value) {
// Yes, is private
if (value) {
this.args.category.set("permissions", []);
// If the user toggles between private and public we
// need to restore whatever previous private permissions were set.
if (this.#previousPermissions) {
this.#setCategoryPermissions(this.#previousPermissions);
} else {
this.#setCategoryPermissions([]);
}
} else {
// No, is public
this.#previousPermissions = [...this.args.category.permissions];
const site = this.args.category.site;
const everyoneGroup = site.groups.find(
(g) => g.id === AUTO_GROUPS.everyone.id
);
this.args.category.set("permissions", [
// By default everyone has full permissions for public categories
// to see and create posts and topics.
this.#setCategoryPermissions([
{
group_name: everyoneGroup?.name || "everyone",
group_id: AUTO_GROUPS.everyone.id,
@@ -128,8 +138,7 @@ export default class UpsertCategoryGeneral extends Component {
});
this.userModifiedPermissions = true;
this.args.category.set("permissions", newPermissions);
this.args.updatePreview?.({});
this.#setCategoryPermissions(newPermissions);
}
@action
@@ -161,8 +170,7 @@ export default class UpsertCategoryGeneral extends Component {
});
this.userModifiedPermissions = true;
this.args.category.set("permissions", newPermissions);
this.args.updatePreview?.({});
this.#setCategoryPermissions(newPermissions);
}
get allowSubCategoriesAsParent() {
@@ -229,7 +237,7 @@ export default class UpsertCategoryGeneral extends Component {
}
@action
onVisibilityChange(value) {
onChangeVisibility(value) {
this.categoryVisibilityState = value;
this.userModifiedPermissions = true;
if (value === "public") {
@@ -237,9 +245,6 @@ export default class UpsertCategoryGeneral extends Component {
} else if (value === "group_restricted") {
this.isPrivateCategory = true;
}
// Trigger preview update to show restricted status
this.args.updatePreview?.({});
}
@action
@@ -250,7 +255,7 @@ export default class UpsertCategoryGeneral extends Component {
const everyoneGroup = site.groups.find(
(g) => g.id === AUTO_GROUPS.everyone.id
);
this.args.category.set("permissions", [
this.#setCategoryPermissions([
{
group_name: everyoneGroup?.name || "everyone",
group_id: AUTO_GROUPS.everyone.id,
@@ -279,13 +284,13 @@ export default class UpsertCategoryGeneral extends Component {
permission_type: p.permission_type,
}));
this.args.category.set("permissions", newPermissions);
this.#setCategoryPermissions(newPermissions);
} else {
const everyoneGroup = site.groups.find(
(g) => g.id === AUTO_GROUPS.everyone.id
);
this.args.category.set("permissions", [
this.#setCategoryPermissions([
{
group_name: everyoneGroup?.name || "everyone",
group_id: AUTO_GROUPS.everyone.id,
@@ -300,8 +305,6 @@ export default class UpsertCategoryGeneral extends Component {
async onParentCategorySet(value, { set, name, index }) {
await set(name, value, { index });
await this.onParentCategoryChange(value);
this.args.updatePreview?.({ parent_category_id: value });
}
@action
@@ -343,39 +346,27 @@ export default class UpsertCategoryGeneral extends Component {
await set("color", color);
if (color) {
const whiteDiff = this.colorDifference(color, CATEGORY_TEXT_COLORS[0]);
const blackDiff = this.colorDifference(color, CATEGORY_TEXT_COLORS[1]);
const whiteDiff = this.#colorDifference(color, CATEGORY_TEXT_COLORS[0]);
const blackDiff = this.#colorDifference(color, CATEGORY_TEXT_COLORS[1]);
const colorIndex = whiteDiff > blackDiff ? 0 : 1;
this.args.form.set("text_color", CATEGORY_TEXT_COLORS[colorIndex]);
this.args.updatePreview?.({
color,
text_color: CATEGORY_TEXT_COLORS[colorIndex],
});
}
}
@action
async onNameChange(value, { set, name, index }) {
await set(name, value, { index });
discourseDebounce(this, this._updateNamePreview, value, INPUT_DELAY);
}
_updateNamePreview(value) {
this.args.updatePreview?.({ name: value });
}
@action
async onIconSet(value, { set, name, index }) {
await set(name, value, { index });
this.args.updatePreview?.({ icon: value, style_type: "icon" });
}
@action
async onEmojiSet(value, { set, name, index }) {
await set(name, value, { index });
this.args.updatePreview?.({ emoji: value, style_type: "emoji" });
}
@action
@@ -395,24 +386,6 @@ export default class UpsertCategoryGeneral extends Component {
} else if (value === "emoji" && currentEmoji) {
updateData.emoji = currentEmoji;
}
this.args.updatePreview?.(updateData);
}
colorDifference(color1, color2) {
const r1 = parseInt(color1.substr(0, 2), 16);
const g1 = parseInt(color1.substr(2, 2), 16);
const b1 = parseInt(color1.substr(4, 2), 16);
const r2 = parseInt(color2.substr(0, 2), 16);
const g2 = parseInt(color2.substr(2, 2), 16);
const b2 = parseInt(color2.substr(4, 2), 16);
const rDiff = Math.max(r1, r2) - Math.min(r1, r2);
const gDiff = Math.max(g1, g2) - Math.min(g1, g2);
const bDiff = Math.max(b1, b2) - Math.min(b1, b2);
return rDiff + gDiff + bDiff;
}
@action
@@ -483,6 +456,26 @@ export default class UpsertCategoryGeneral extends Component {
return `edit-category-tab edit-category-tab-general ${isActive}`;
}
#colorDifference(color1, color2) {
const r1 = parseInt(color1.substr(0, 2), 16);
const g1 = parseInt(color1.substr(2, 2), 16);
const b1 = parseInt(color1.substr(4, 2), 16);
const r2 = parseInt(color2.substr(0, 2), 16);
const g2 = parseInt(color2.substr(2, 2), 16);
const b2 = parseInt(color2.substr(4, 2), 16);
const rDiff = Math.max(r1, r2) - Math.min(r1, r2);
const gDiff = Math.max(g1, g2) - Math.min(g1, g2);
const bDiff = Math.max(b1, b2) - Math.min(b1, b2);
return rDiff + gDiff + bDiff;
}
#setCategoryPermissions(permissions) {
this.args.category.set("permissions", permissions);
}
<template>
<div class={{this.panelClass}}>
{{#if this.showWarning}}
@@ -651,7 +644,7 @@ export default class UpsertCategoryGeneral extends Component {
>
<@form.ConditionalContent
@activeName={{this.categoryVisibility}}
@onChange={{this.onVisibilityChange}}
@onChange={{this.onChangeVisibility}}
as |cc|
>
<cc.Conditions as |Condition|>
@@ -4,7 +4,6 @@ import { action, getProperties } from "@ember/object";
import { and } from "@ember/object/computed";
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { AUTO_GROUPS } from "discourse/lib/constants";
import discourseComputed from "discourse/lib/decorators";
@@ -49,26 +48,6 @@ const FIELD_LIST = [
"mailinglist_mirror",
];
const PREVIEW_FIELD_MAP = {
name: "previewName",
color: "previewColor",
text_color: "previewTextColor",
style_type: "previewStyleType",
emoji: "previewEmoji",
icon: "previewIcon",
parent_category_id: "previewParentCategoryId",
};
const PREVIEW_DEFAULTS = {
previewName: "",
previewColor: "",
previewTextColor: "",
previewStyleType: "",
previewEmoji: "",
previewIcon: "",
previewParentCategoryId: null,
};
const SHOW_ADVANCED_TABS_KEY = "category_edit_show_advanced_tabs";
export default class EditCategoryTabsController extends Controller {
@@ -83,7 +62,6 @@ export default class EditCategoryTabsController extends Controller {
showAdvancedTabs =
this.keyValueStore.getItem(SHOW_ADVANCED_TABS_KEY) === "true";
@tracked selectedTab = "general";
@tracked previewData = new TrackedObject(PREVIEW_DEFAULTS);
@trackedArray panels = [];
saving = false;
deleting = false;
@@ -135,22 +113,6 @@ export default class EditCategoryTabsController extends Controller {
return i18n("category.create");
}
@action
updatePreview(data) {
Object.entries(PREVIEW_FIELD_MAP).forEach(([key, previewField]) => {
if (data[key] !== undefined) {
this.previewData[previewField] = data[key];
}
});
}
@action
resetPreview() {
Object.entries(PREVIEW_DEFAULTS).forEach(([key, value]) => {
this.previewData[key] = value;
});
}
@action
setSelectedTab(tab) {
this.selectedTab = tab;
@@ -17,7 +17,5 @@ export default class EditCategoryTabs extends DiscourseRoute {
});
controller.setSelectedTab(selectedTab);
controller.resetPreview();
}
}
@@ -68,7 +68,6 @@ export default class NewCategory extends DiscourseRoute {
setupController(controller) {
super.setupController(...arguments);
controller.resetPreview();
controller.selectedTab = "general";
}
@@ -80,7 +80,6 @@ export default <template>
@registerValidator={{@controller.registerValidator}}
@transientData={{transientData}}
@form={{form}}
@updatePreview={{@controller.updatePreview}}
@setSelectedTab={{@controller.setSelectedTab}}
/>
{{/let}}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 48 KiB