diff --git a/app/assets/stylesheets/common/base/_index.scss b/app/assets/stylesheets/common/base/_index.scss
index 8f832040b98..48ea659933c 100644
--- a/app/assets/stylesheets/common/base/_index.scss
+++ b/app/assets/stylesheets/common/base/_index.scss
@@ -17,6 +17,7 @@
@import "directory";
@import "discourse";
@import "edit-category";
+@import "uc-simplified-category-setup-edit-categories";
@import "edit-topic-timer-modal";
@import "ember-select";
@import "emoji";
diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss
index 442fd2da2f9..f252363b475 100644
--- a/app/assets/stylesheets/common/base/discourse.scss
+++ b/app/assets/stylesheets/common/base/discourse.scss
@@ -405,7 +405,6 @@ input {
&::-webkit-color-swatch {
border: none;
- border-radius: 0;
}
}
diff --git a/app/assets/stylesheets/common/base/edit-category.scss b/app/assets/stylesheets/common/base/edit-category.scss
index 2783c9b2f84..02098a3ba3e 100644
--- a/app/assets/stylesheets/common/base/edit-category.scss
+++ b/app/assets/stylesheets/common/base/edit-category.scss
@@ -120,6 +120,10 @@ div.edit-category {
.form-kit__container-content {
flex-wrap: wrap;
}
+
+ .category-permission-hint {
+ font-size: var(--font-down-1);
+ }
}
#list-area & h2 {
@@ -146,6 +150,19 @@ div.edit-category {
.category-chooser {
width: min(340px, 90%);
}
+
+ .select-kit-header {
+ padding-right: 0.65em;
+
+ &:hover {
+ border: 1px solid var(--tertiary);
+ }
+
+ .caret-icon {
+ color: var(--primary-medium);
+ width: 0.85em;
+ }
+ }
}
.edit-category-tab-security {
diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss
index 451ec7175f0..043efa02e4c 100644
--- a/app/assets/stylesheets/common/base/sidebar.scss
+++ b/app/assets/stylesheets/common/base/sidebar.scss
@@ -162,6 +162,16 @@
color: var(--d-sidebar-link-icon-color);
}
}
+
+ .select-kit.single-select.dropdown-select-box .select-kit-row {
+ .icons .d-icon {
+ font-size: var(--font-0);
+ }
+
+ .texts .name {
+ font-weight: normal;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/common/base/uc-simplified-category-setup-edit-categories.scss b/app/assets/stylesheets/common/base/uc-simplified-category-setup-edit-categories.scss
new file mode 100644
index 00000000000..1a1fbff0ef3
--- /dev/null
+++ b/app/assets/stylesheets/common/base/uc-simplified-category-setup-edit-categories.scss
@@ -0,0 +1,98 @@
+.uc-enable-simplified-category-creation {
+ .select-kit.multi-select.group-chooser {
+ width: var(--form-kit-large-input);
+ }
+
+ div.edit-category {
+ display: block;
+
+ .add-required-tag-group {
+ align-self: start;
+ }
+
+ .form-kit__control-icon {
+ .select-kit-row .d-icon,
+ .select-kit-header .d-icon,
+ .select-kit-header-wrapper .d-icon:first-of-type {
+ color: var(--icon-color, currentColor);
+ }
+
+ .select-kit-header-wrapper {
+ gap: var(--space-2);
+ }
+ }
+
+ .form-kit__control-icon .formatted-selection {
+ display: flex !important; // overrides another important
+ flex: 0 1 auto;
+ }
+
+ #control-emoji {
+ &.form-kit__field .form-kit__container-content {
+ width: auto;
+ }
+ }
+
+ .edit-category-title-bar {
+ box-sizing: border-box;
+ width: 100%;
+ flex-wrap: wrap;
+ }
+
+ .edit-category-title {
+ flex: 1 1 auto;
+
+ h2 {
+ margin-top: 0;
+ }
+
+ .btn-back {
+ margin-top: -0.5em;
+ margin-left: -0.5em;
+ margin-bottom: 0.5em;
+ }
+ }
+
+ .edit-category-nav {
+ border-bottom: 1px solid var(--content-border-color);
+ }
+
+ .edit-category-content {
+ margin-top: 2em;
+ grid-area: unset;
+ }
+ }
+
+ .edit-category-tab-security {
+ .form-kit__container-content {
+ width: 100%;
+ }
+ }
+
+ .edit-category-title-text {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+
+ .edit-category-static-title {
+ transition: font-size 0.2s ease-in-out;
+ }
+
+ &:has(.badge-category__wrapper) {
+ .edit-category-static-title {
+ position: absolute;
+ font-size: var(--font-down-3);
+ }
+ }
+
+ .badge-category__wrapper {
+ font-weight: normal;
+ font-size: var(--font-up-2);
+ margin-top: var(--space-5);
+ }
+ }
+
+ .category-permissions-table {
+ width: 100%;
+ }
+}
diff --git a/app/assets/stylesheets/common/components/d-page-header.scss b/app/assets/stylesheets/common/components/d-page-header.scss
index 1d3dd67983c..9d0f640b3e0 100644
--- a/app/assets/stylesheets/common/components/d-page-header.scss
+++ b/app/assets/stylesheets/common/components/d-page-header.scss
@@ -4,6 +4,7 @@ $mobile-breakpoint: 700px;
.d-page-subheader {
&__title-row {
display: flex;
+ flex-wrap: wrap;
justify-content: space-between;
align-items: stretch;
margin-bottom: var(--space-2);
@@ -74,6 +75,12 @@ $mobile-breakpoint: 700px;
}
}
}
+
+ &__drawer {
+ display: flex;
+ align-items: center;
+ margin-bottom: var(--space-2);
+ }
}
.d-page-subheader {
diff --git a/app/assets/stylesheets/common/form-kit/_conditional-display.scss b/app/assets/stylesheets/common/form-kit/_conditional-display.scss
index 87c020f1445..98082bb3e3d 100644
--- a/app/assets/stylesheets/common/form-kit/_conditional-display.scss
+++ b/app/assets/stylesheets/common/form-kit/_conditional-display.scss
@@ -1,5 +1,83 @@
.form-kit__conditional-display {
- .form-kit__inline-radio {
- padding-bottom: 0.25rem;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+
+ input[disabled] + span {
+ color: var(--primary-medium);
+ cursor: not-allowed;
+ }
+
+ &-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+ }
+}
+
+.--radio-cards {
+ width: var(--form-kit-large-input);
+
+ .form-kit__conditional-display,
+ .form-kit__container-content {
+ width: 100%;
+ }
+
+ .form-kit__inline-radio {
+ gap: var(--space-2);
+ width: 100%;
+
+ label:hover {
+ cursor: pointer;
+ border: 1px solid var(--tertiary);
+ }
+
+ > * {
+ width: 50%;
+ }
+
+ .fk-d-tooltip__trigger-container {
+ width: 100%;
+ }
+ }
+
+ .form-kit__control-radio-label {
+ flex: 1 1 50%;
+ padding: var(--space-2);
+ border: 1px solid var(--primary-low-mid);
+ border-radius: var(--d-input-border-radius);
+ justify-content: center;
+ position: relative;
+
+ .d-icon {
+ display: none;
+ }
+
+ input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+ }
+
+ &:has(input[disabled]) {
+ background: var(--primary-very-low);
+
+ .d-icon {
+ color: currentcolor;
+ display: inline-block;
+ }
+ }
+
+ &:has(input:checked) {
+ color: var(--tertiary);
+ border-color: var(--tertiary);
+ background: var(--tertiary-very-low);
+
+ .d-icon {
+ color: currentcolor;
+ display: inline-block;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/common/form-kit/_control-color.scss b/app/assets/stylesheets/common/form-kit/_control-color.scss
index 3611823b937..496ba4df860 100644
--- a/app/assets/stylesheets/common/form-kit/_control-color.scss
+++ b/app/assets/stylesheets/common/form-kit/_control-color.scss
@@ -8,9 +8,15 @@
}
.form-kit__control-color-input {
+ --color-button-height: var(--space-8);
+ --color-button-width: var(--space-8);
display: flex;
align-items: center;
gap: 0.25em;
+
+ @include viewport.until(sm) {
+ --color-button-height: var(--space-9);
+ }
}
.form-kit__control-color-input-prefix {
@@ -24,10 +30,6 @@
align-items: center;
margin-right: -0.25em;
border-radius: var(--d-input-border-radius) 0 0 var(--d-input-border-radius);
-
- @include viewport.until(sm) {
- height: 2.25em;
- }
}
.form-kit__control-color-input-hex {
@@ -50,18 +52,36 @@
}
}
+.form-kit__control-color-picker-wrapper {
+ position: relative;
+ display: inline-flex;
+
+ .d-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ font-size: var(--font-down-1);
+ }
+
+ &.--is-light .d-icon {
+ color: black;
+ }
+
+ &.--is-dark .d-icon {
+ color: white;
+ }
+}
+
.form-kit__control-color-input-picker {
- width: 2em;
- height: 2em;
+ width: var(--color-button-width);
+ height: var(--color-button-height);
padding: 0;
border: 1px solid var(--primary-low-mid);
border-radius: var(--d-input-border-radius);
cursor: pointer;
- @include viewport.until(sm) {
- height: 2.25em;
- }
-
&::-webkit-color-swatch-wrapper {
padding: 2px;
}
@@ -91,10 +111,20 @@
}
}
+.form-kit__control-color-swatches-btn {
+ --d-button-default-border: 1px solid var(--d-button-default-bg-color); // match input height
+ width: var(--color-button-width);
+ height: var(--color-button-height);
+}
+
.form-kit__control-color-swatches {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
+
+ .color-swatches-menu-content & {
+ padding: var(--space-2);
+ }
}
.form-kit__control-color-swatch {
@@ -107,7 +137,6 @@
display: flex;
align-items: center;
justify-content: center;
- color: white;
.d-icon {
display: none;
@@ -116,7 +145,14 @@
&.is-used .d-icon {
display: inline-flex;
- opacity: 0.8;
+ }
+
+ &.--is-light .d-icon {
+ color: black;
+ }
+
+ &.--is-dark .d-icon {
+ color: white;
}
&:hover {
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index d369a8080da..a7a93c4e068 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -4525,8 +4525,11 @@ en:
edit_dialog_title: "Edit: %{categoryName}"
view: "View Topics in Category"
back: "Back to category"
+ single_page: "Single Page"
general: "General"
settings: "Settings"
+ advanced: "Advanced"
+ show_advanced: "Advanced settings"
topic_template: "Template"
tags: "Tags"
localizations: "Localizations"
@@ -4557,6 +4560,7 @@ en:
create_long: "Create a new category"
save: "Save category"
slug: "Category slug"
+ parent: "Parent category"
slug_placeholder: "(Optional) dashed-words for url"
creation_error: There has been an error during the creation of the category.
save_error: There was an error saving the category.
@@ -4570,7 +4574,10 @@ en:
logo_description: "Recommended 1:1 aspect ratio with 200px minimum size. If left blank no image will be shown."
background_image: "Category background image"
background_image_dark: "Dark category background image"
- style: "Styles"
+ style: "Style"
+ icon: "Icon"
+ emoji: "Emoji"
+ select_emoji: "Select emoji"
background_color: "Color"
foreground_color: "Text color"
color_validations:
@@ -4581,13 +4588,14 @@ en:
type: "Style"
icon: "Icon"
emoji: "Emoji"
+ square: "Square"
options:
square: "Square"
icon: "Icon"
emoji: "Emoji"
color_used: "Color in use"
predefined_colors: "Predefined color options"
- name_placeholder: "One or two words maximum"
+ name_placeholder: "One or two words recommended"
color_placeholder: "Any web color"
delete_confirm: "Are you sure you want to delete this category?"
delete_error: "There was an error deleting the category."
@@ -4595,6 +4603,7 @@ en:
no_description: "Please add a description for this category."
change_in_category_topic: "Edit description"
already_used: "This color has been used by another category"
+ color_palette: "Color palette"
security: "Security"
security_add_group: "Add a group"
permissions:
@@ -4604,6 +4613,7 @@ en:
create: "Create"
no_groups_selected: "No groups have been granted access; this category will only be visible to staff."
everyone_has_access: 'This category is public, everyone can see, reply and create posts. To restrict permissions, remove one or more of the permissions granted to the "everyone" group.'
+ specific_groups_have_access: "This category is private, only the selected groups can see, reply and create posts."
toggle_reply: "Toggle Reply permission"
toggle_full: "Toggle Create permission"
inherited: 'This permission is inherited from "everyone"'
@@ -4611,7 +4621,7 @@ en:
uncategorized_security_warning: "This category is special. It is intended as holding area for topics that have no category; it cannot have security settings."
uncategorized_general_warning: 'This category is special. It is used as the default category for new topics that do not have a category selected. If you want to prevent this behavior and force category selection, please disable the setting here. If you want to change the name or description, go to Customize / Text Content.'
pending_permission_change_alert: "You haven't added %{group} to this category; click this button to add them."
- images: "Images"
+ images: "Appearance"
email_in: "Custom incoming email address:"
email_in_tooltip: "You can separate multiple email addresses with the | character."
email_in_allow_strangers: "Accept emails from anonymous users with no accounts"
@@ -4640,7 +4650,18 @@ en:
position_disabled: "Categories will be displayed in order of activity. To control the order of categories in lists, enable the 'fixed category positions' setting."
minimum_required_tags: "Minimum number of tags required in a topic:"
default_slow_mode: 'Enable "Slow mode" for new topics in this category.'
- parent: "Parent category"
+ subcategory_of: "Subcategory of..."
+ none_subcategory_text: "None"
+ subcategory_permissions_warning: "A subcategory of a private category can not be public."
+ visibility:
+ title: "Access"
+ public: "Public"
+ all_members: "All members"
+ group_restricted: "Private"
+ who_can_see: "Who can see this category"
+ who_can_post: "Who can post in this category"
+ more_options_hint: "More permissions available in"
+ more_options_hint_link: "advanced settings."
num_auto_bump_daily: "Number of open topics to automatically bump daily:"
auto_bump_cooldown_days: "Minimum days before bumping the same topic again:"
navigate_to_first_post_after_read: "Navigate to first post after topics are read"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 71d9e10ee9f..472c110177c 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -2752,6 +2752,7 @@ en:
fake_upcoming_change: "This is a fake upcoming change for testing purposes. No need to translate this string."
floating_dismiss_topics_on_mobile: "Shows a floating 'Dismiss...' button on mobile for the topic list for easier access and to free up space in the top of the topic list"
experimental_rename_faq_to_guidelines: "This change renames the FAQ page to Guidelines. The FAQ page will still be available at /faq. The 'faq url' setting works as before."
+ enable_simplified_category_creation: "Enables a simplified category creation flow with fewer options and a cleaner interface."
always_include_topic_excerpts: "Includes topic excerpts in topic lists like Latest and so on. This excerpt may not be visible unless a theme component like 'Topic List Excerpts' is enabled."
errors:
diff --git a/config/site_settings.yml b/config/site_settings.yml
index df4f3e1f48f..ad1c8c5ed04 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -4226,3 +4226,10 @@ experimental:
upcoming_change:
status: "experimental"
impact: "feature,all_members"
+ enable_simplified_category_creation:
+ default: false
+ hidden: true
+ client: true
+ upcoming_change:
+ status: "conceptual"
+ impact: "feature,staff"
diff --git a/frontend/discourse/admin/components/edit-category-general-v2.gjs b/frontend/discourse/admin/components/edit-category-general-v2.gjs
new file mode 100644
index 00000000000..451d4741298
--- /dev/null
+++ b/frontend/discourse/admin/components/edit-category-general-v2.gjs
@@ -0,0 +1,678 @@
+import Component from "@glimmer/component";
+import { cached, tracked } from "@glimmer/tracking";
+import { concat, hash } from "@ember/helper";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import { service } from "@ember/service";
+import { htmlSafe } from "@ember/template";
+import EmojiPicker from "discourse/components/emoji-picker";
+import DTooltip from "discourse/float-kit/components/d-tooltip";
+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";
+import CategoryChooser from "discourse/select-kit/components/category-chooser";
+import GroupChooser from "discourse/select-kit/components/group-chooser";
+import IconPicker from "discourse/select-kit/components/icon-picker";
+import { or } from "discourse/truth-helpers";
+import { i18n } from "discourse-i18n";
+
+export default class EditCategoryGeneralV2 extends Component {
+ @service site;
+ @service siteSettings;
+
+ @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"
+ );
+
+ get parentIsRestricted() {
+ const parentId = this.args.transientData.parent_category_id;
+ if (!parentId) {
+ return false;
+ }
+
+ const parentCategory = Category.findById(parentId);
+ if (!parentCategory?.permissions?.length) {
+ return false;
+ }
+
+ const onlyEveryone =
+ parentCategory.permissions.length === 1 &&
+ (parentCategory.permissions[0].group_id === AUTO_GROUPS.everyone.id ||
+ parentCategory.permissions[0].group_name === "everyone");
+
+ return !onlyEveryone;
+ }
+
+ get isPrivateCategory() {
+ const permissions = this.args.category.permissions;
+
+ if (!permissions || permissions.length === 0) {
+ return true;
+ }
+
+ const onlyEveryone =
+ permissions.length === 1 &&
+ (permissions[0].group_id === AUTO_GROUPS.everyone.id ||
+ permissions[0].group_name === "everyone");
+
+ return !onlyEveryone;
+ }
+
+ set isPrivateCategory(value) {
+ if (value) {
+ this.args.category.set("permissions", []);
+ } else {
+ const site = this.args.category.site;
+ const everyoneGroup = site.groups.find(
+ (g) => g.id === AUTO_GROUPS.everyone.id
+ );
+
+ this.args.category.set("permissions", [
+ {
+ group_name: everyoneGroup?.name || "everyone",
+ group_id: AUTO_GROUPS.everyone.id,
+ permission_type: PermissionType.FULL,
+ },
+ ]);
+ }
+ }
+
+ get visibilityGroups() {
+ const permissions = this.args.category.permissions || [];
+ return permissions
+ .filter((p) => p.permission_type <= PermissionType.READONLY)
+ .map((p) => p.group_id);
+ }
+
+ get postingGroups() {
+ const permissions = this.args.category.permissions || [];
+ return permissions
+ .filter((p) => p.permission_type <= PermissionType.CREATE_POST)
+ .map((p) => p.group_id);
+ }
+
+ @action
+ onChangeVisibilityGroups(groupIds) {
+ const site = this.args.category.site;
+ const currentPermissions = this.args.category.permissions || [];
+
+ const postingGroupPermissions = new Map(
+ currentPermissions
+ .filter((p) => p.permission_type <= PermissionType.CREATE_POST)
+ .map((p) => [p.group_id, p.permission_type])
+ );
+
+ const newPermissions = [];
+
+ groupIds.forEach((groupId) => {
+ const group = site.groups.find((g) => g.id === groupId);
+ const existingPermission = postingGroupPermissions.get(groupId);
+
+ newPermissions.push({
+ group_name: group?.name,
+ group_id: groupId,
+ permission_type: existingPermission ?? PermissionType.READONLY,
+ });
+ });
+
+ this.userModifiedPermissions = true;
+ this.args.category.set("permissions", newPermissions);
+ this.args.updatePreview?.({});
+ }
+
+ @action
+ onChangePostingGroups(groupIds) {
+ // Posting groups automatically get visibility (if they can post, they can see)
+ const site = this.args.category.site;
+ const currentPermissions = this.args.category.permissions || [];
+ const currentVisibilityGroupIds = currentPermissions.map((p) => p.group_id);
+ const newPermissions = [];
+
+ groupIds.forEach((groupId) => {
+ const group = site.groups.find((g) => g.id === groupId);
+ newPermissions.push({
+ group_name: group?.name,
+ group_id: groupId,
+ permission_type: PermissionType.FULL,
+ });
+ });
+
+ currentVisibilityGroupIds.forEach((groupId) => {
+ if (!groupIds.includes(groupId)) {
+ const group = site.groups.find((g) => g.id === groupId);
+ newPermissions.push({
+ group_name: group?.name,
+ group_id: groupId,
+ permission_type: PermissionType.READONLY,
+ });
+ }
+ });
+
+ this.userModifiedPermissions = true;
+ this.args.category.set("permissions", newPermissions);
+ this.args.updatePreview?.({});
+ }
+
+ get allowSubCategoriesAsParent() {
+ // If max_category_nesting is 2, only top-level categories can be parents
+ // If max_category_nesting is 3, subcategories can also be parents
+ return this.siteSettings.max_category_nesting > 2;
+ }
+
+ @cached
+ get backgroundColors() {
+ const categories = this.site.get("categoriesList");
+ return uniqueItemsFromArray(
+ this.siteSettings.category_colors
+ .split("|")
+ .filter(Boolean)
+ .map((i) => i.toUpperCase())
+ .concat(categories.map((c) => c.color.toUpperCase()))
+ );
+ }
+
+ @cached
+ get usedBackgroundColors() {
+ const categories = this.site.get("categoriesList");
+ const categoryId = this.args.category.id;
+ const categoryColor = this.args.category.color;
+
+ return categories
+ .map((c) => {
+ return categoryId &&
+ categoryColor?.toUpperCase() === c.color.toUpperCase()
+ ? null
+ : c.color.toUpperCase();
+ })
+ .filter((item) => item != null);
+ }
+
+ get categoryVisibility() {
+ if (this.categoryVisibilityState) {
+ return this.categoryVisibilityState;
+ }
+
+ if (this.parentIsRestricted) {
+ return "group_restricted";
+ }
+
+ return this.isPrivateCategory ? "group_restricted" : "public";
+ }
+
+ get publicVisibilityLabel() {
+ return this.siteSettings.login_required
+ ? "category.visibility.all_members"
+ : "category.visibility.public";
+ }
+
+ @action
+ onVisibilityChange(value) {
+ this.categoryVisibilityState = value;
+ this.userModifiedPermissions = true;
+ if (value === "public") {
+ this.isPrivateCategory = false;
+ } else if (value === "group_restricted") {
+ this.isPrivateCategory = true;
+ }
+
+ // Trigger preview update to show restricted status
+ this.args.updatePreview?.({});
+ }
+
+ @action
+ async onParentCategoryChange(parentCategoryId) {
+ if (!parentCategoryId) {
+ if (!this.userModifiedPermissions) {
+ const site = this.args.category.site;
+ const everyoneGroup = site.groups.find(
+ (g) => g.id === AUTO_GROUPS.everyone.id
+ );
+ this.args.category.set("permissions", [
+ {
+ group_name: everyoneGroup?.name || "everyone",
+ group_id: AUTO_GROUPS.everyone.id,
+ permission_type: PermissionType.FULL,
+ },
+ ]);
+ }
+ return;
+ }
+
+ const result = await Category.reloadById(parentCategoryId);
+ const site = this.args.category.site;
+ const parentCategory = site.updateCategory(result.category);
+ parentCategory.setupGroupsAndPermissions();
+
+ if (parentCategory?.permissions?.length > 0) {
+ const onlyEveryone =
+ parentCategory.permissions.length === 1 &&
+ (parentCategory.permissions[0].group_id === AUTO_GROUPS.everyone.id ||
+ parentCategory.permissions[0].group_name === "everyone");
+
+ if (!onlyEveryone) {
+ const newPermissions = parentCategory.permissions.map((p) => ({
+ group_name: p.group_name,
+ group_id: p.group_id,
+ permission_type: p.permission_type,
+ }));
+
+ this.args.category.set("permissions", newPermissions);
+ } else {
+ const everyoneGroup = site.groups.find(
+ (g) => g.id === AUTO_GROUPS.everyone.id
+ );
+
+ this.args.category.set("permissions", [
+ {
+ group_name: everyoneGroup?.name || "everyone",
+ group_id: AUTO_GROUPS.everyone.id,
+ permission_type: PermissionType.FULL,
+ },
+ ]);
+ }
+ }
+ }
+
+ @action
+ async onParentCategorySet(value, { set, name, index }) {
+ await set(name, value, { index });
+ await this.onParentCategoryChange(value);
+
+ this.args.updatePreview?.({ parent_category_id: value });
+ }
+
+ @action
+ togglePrivateCategory() {
+ this.isPrivateCategory = !this.isPrivateCategory;
+ }
+
+ get showWarning() {
+ return this.args.category.isUncategorizedCategory;
+ }
+
+ @cached
+ get subCategories() {
+ if (this.args.category.isNew) {
+ return null;
+ }
+ return Category.list().filter(
+ (category) => category.get("parent_category_id") === this.args.category.id
+ );
+ }
+
+ @cached
+ get showDescription() {
+ const category = this.args.category;
+ return (
+ !category.isUncategorizedCategory && category.id && category.topic_url
+ );
+ }
+
+ @action
+ showCategoryTopic() {
+ window.open(this.args.category.get("topic_url"), "_blank").focus();
+ }
+
+ @action
+ async onBackgroundColorSet(value, { set }) {
+ const color = value?.replace(/^#/, "") ?? "";
+
+ await set("color", color);
+
+ 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
+ onStyleTypeChange(value) {
+ this.args.form.setProperties({ style_type: value });
+
+ const updateData = { style_type: value };
+
+ // Use transientData (current form values) if available, otherwise fall back to category model
+ const currentIcon =
+ this.args.transientData?.icon ?? this.args.category.icon ?? null;
+ const currentEmoji =
+ this.args.transientData?.emoji ?? this.args.category.emoji ?? null;
+
+ if (value === "icon" && currentIcon) {
+ updateData.icon = currentIcon;
+ } 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
+ validateColor(name, color, { addError }) {
+ color = color.trim();
+
+ let title;
+ if (name === "color") {
+ title = i18n("category.background_color");
+ } else if (name === "text_color") {
+ title = i18n("category.foreground_color");
+ } else {
+ throw new Error(`unknown title for category attribute ${name}`);
+ }
+
+ if (!color) {
+ addError(name, {
+ title,
+ message: i18n("category.color_validations.cant_be_empty"),
+ });
+ }
+
+ if (color.length !== 3 && color.length !== 6) {
+ addError(name, {
+ title,
+ message: i18n("category.color_validations.incorrect_length"),
+ });
+ }
+
+ if (!/^[0-9A-Fa-f]+$/.test(color)) {
+ addError(name, {
+ title,
+ message: i18n("category.color_validations.non_hexdecimal"),
+ });
+ }
+ }
+
+ get categoryDescription() {
+ if (this.args.category.description) {
+ return htmlSafe(this.args.category.description);
+ }
+
+ return htmlSafe(i18n("category.no_description"));
+ }
+
+ @action
+ goToSecurityTab(event) {
+ event.preventDefault();
+ this.args.setSelectedTab?.("security");
+ }
+
+ get canSelectParentCategory() {
+ return !this.args.category.isUncategorizedCategory;
+ }
+
+ get panelClass() {
+ const isActive = this.args.selectedTab === "general" ? "active" : "";
+ return `edit-category-tab edit-category-tab-general ${isActive}`;
+ }
+
+
+
{{i18n - "category.permissions.everyone_has_access" - }}
+ <@form.Alert @type="warning"> + {{i18n "category.permissions.everyone_has_access"}} + @form.Alert> + {{else}} + <@form.Alert @type="warning"> + {{i18n "category.permissions.specific_groups_have_access"}} + @form.Alert> {{/if}} {{/unless}} diff --git a/frontend/discourse/admin/components/edit-category-settings-v2.gjs b/frontend/discourse/admin/components/edit-category-settings-v2.gjs new file mode 100644 index 00000000000..eea1c961eac --- /dev/null +++ b/frontend/discourse/admin/components/edit-category-settings-v2.gjs @@ -0,0 +1,418 @@ +import { Input } from "@ember/component"; +import { fn, hash } from "@ember/helper"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { and, empty } from "@ember/object/computed"; +import { htmlSafe } from "@ember/template"; +import { buildCategoryPanel } from "discourse/admin/components/edit-category-panel"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import RelativeTimePicker from "discourse/components/relative-time-picker"; +import lazyHash from "discourse/helpers/lazy-hash"; +import withEventValue from "discourse/helpers/with-event-value"; +import { setting } from "discourse/lib/computed"; +import { SEARCH_PRIORITIES } from "discourse/lib/constants"; +import discourseComputed from "discourse/lib/decorators"; +import getUrl from "discourse/lib/get-url"; +import { applyMutableValueTransformer } from "discourse/lib/transformer"; +import ComboBox from "discourse/select-kit/components/combo-box"; +import GroupChooser from "discourse/select-kit/components/group-chooser"; +import { i18n } from "discourse-i18n"; + +export default class EditCategorySettingsV2 extends buildCategoryPanel( + "settings" +) { + @setting("email_in") emailInEnabled; + @setting("fixed_category_positions") showPositionInput; + + @and("category.show_subcategory_list", "isParentCategory") + showSubcategoryListStyle; + @empty("category.sort_order") isDefaultSortOrder; + + @discourseComputed( + "category.isParent", + "category.parent_category_id", + "transientData.parent_category_id" + ) + isParentCategory(isParent, parentCategoryId, transientParentCategoryId) { + return isParent || !(parentCategoryId || transientParentCategoryId); + } + + @discourseComputed + availableSubcategoryListStyles() { + return [ + { name: i18n("category.subcategory_list_styles.rows"), value: "rows" }, + { + name: i18n( + "category.subcategory_list_styles.rows_with_featured_topics" + ), + value: "rows_with_featured_topics", + }, + { + name: i18n("category.subcategory_list_styles.boxes"), + value: "boxes", + }, + { + name: i18n( + "category.subcategory_list_styles.boxes_with_featured_topics" + ), + value: "boxes_with_featured_topics", + }, + ]; + } + + @discourseComputed("category.id", "category.custom_fields") + availableViews(categoryId, customFields) { + const views = [ + { name: i18n("filters.hot.title"), value: "hot" }, + { name: i18n("filters.latest.title"), value: "latest" }, + { name: i18n("filters.top.title"), value: "top" }, + ]; + + const context = { + categoryId, + customFields, + }; + + return applyMutableValueTransformer( + "category-available-views", + views, + context + ); + } + + @discourseComputed + availableTopPeriods() { + return ["all", "yearly", "quarterly", "monthly", "weekly", "daily"].map( + (p) => { + return { name: i18n(`filters.top.${p}.title`), value: p }; + } + ); + } + + @discourseComputed + availableListFilters() { + return ["all", "none"].map((p) => { + return { name: i18n(`category.list_filters.${p}`), value: p }; + }); + } + + @discourseComputed + searchPrioritiesOptions() { + const options = []; + + Object.entries(SEARCH_PRIORITIES).forEach((entry) => { + const [name, value] = entry; + + options.push({ + name: i18n(`category.search_priority.options.${name}`), + value, + }); + }); + + return options; + } + + @discourseComputed + availableSorts() { + return applyMutableValueTransformer("category-sort-orders", [ + "likes", + "op_likes", + "views", + "posts", + "activity", + "posters", + "category", + "created", + ]) + .map((s) => ({ name: i18n("category.sort_options." + s), value: s })) + .sort((a, b) => a.name.localeCompare(b.name)); + } + + @discourseComputed("category.sort_ascending") + sortAscendingOption(sortAscending) { + if (sortAscending === "false") { + return false; + } + if (sortAscending === "true") { + return true; + } + return sortAscending; + } + + @discourseComputed + sortAscendingOptions() { + return [ + { name: i18n("category.sort_ascending"), value: true }, + { name: i18n("category.sort_descending"), value: false }, + ]; + } + + @discourseComputed + hiddenRelativeIntervals() { + return ["mins"]; + } + + @action + onAutoCloseDurationChange(minutes) { + let hours = minutes ? minutes / 60 : null; + this.set("category.auto_close_hours", hours); + } + + @action + onDefaultSlowModeDurationChange(minutes) { + let seconds = minutes ? minutes * 60 : null; + this.set("category.default_slow_mode_seconds", seconds); + } + + @action + onCategoryModeratingGroupsChange(groupIds) { + this.set("category.moderating_group_ids", groupIds); + } + + + {{! This field is removed from edit-category-general when the UC is active }} + {{#if this.siteSettings.enable_simplified_category_creation}} + <@form.Field + @name="slug" + @title={{i18n "category.slug"}} + @format="large" + as |field| + > +