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}`; + } + + +} diff --git a/frontend/discourse/admin/components/edit-category-images-v2.gjs b/frontend/discourse/admin/components/edit-category-images-v2.gjs new file mode 100644 index 00000000000..bf24536c93d --- /dev/null +++ b/frontend/discourse/admin/components/edit-category-images-v2.gjs @@ -0,0 +1,390 @@ +import { fn, hash } from "@ember/helper"; +import EmberObject, { action } from "@ember/object"; +import { and, empty } from "@ember/object/computed"; +import { buildCategoryPanel } from "discourse/admin/components/edit-category-panel"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import UppyImageUploader from "discourse/components/uppy-image-uploader"; +import lazyHash from "discourse/helpers/lazy-hash"; +import { CATEGORY_TEXT_COLORS } from "discourse/lib/constants"; +import discourseComputed from "discourse/lib/decorators"; +import { applyMutableValueTransformer } from "discourse/lib/transformer"; +import ComboBox from "discourse/select-kit/components/combo-box"; +import { i18n } from "discourse-i18n"; + +export default class EditCategoryImagesV2 extends buildCategoryPanel("images") { + @and("category.show_subcategory_list", "isParentCategory") + showSubcategoryListStyle; + + @empty("category.sort_order") isDefaultSortOrder; + + @discourseComputed("category.uploaded_background.url") + backgroundImageUrl(uploadedBackgroundUrl) { + return uploadedBackgroundUrl || ""; + } + + @discourseComputed("category.uploaded_background_dark.url") + backgroundDarkImageUrl(uploadedBackgroundDarkUrl) { + return uploadedBackgroundDarkUrl || ""; + } + + @discourseComputed("category.uploaded_logo.url") + logoImageUrl(uploadedLogoUrl) { + return uploadedLogoUrl || ""; + } + + @discourseComputed("category.uploaded_logo_dark.url") + logoImageDarkUrl(uploadedLogoDarkUrl) { + return uploadedLogoDarkUrl || ""; + } + + @action + logoUploadDone(upload) { + this._setFromUpload("category.uploaded_logo", upload); + } + + @action + logoUploadDeleted() { + this._deleteUpload("category.uploaded_logo"); + } + + @action + logoDarkUploadDone(upload) { + this._setFromUpload("category.uploaded_logo_dark", upload); + } + + @action + logoDarkUploadDeleted() { + this._deleteUpload("category.uploaded_logo_dark"); + } + + @action + backgroundUploadDone(upload) { + this._setFromUpload("category.uploaded_background", upload); + } + + @action + backgroundUploadDeleted() { + this._deleteUpload("category.uploaded_background"); + } + + @action + backgroundDarkUploadDone(upload) { + this._setFromUpload("category.uploaded_background_dark", upload); + } + + @action + backgroundDarkUploadDeleted() { + this._deleteUpload("category.uploaded_background_dark"); + } + + _deleteUpload(path) { + this.set( + path, + EmberObject.create({ + id: null, + url: null, + }) + ); + } + + _setFromUpload(path, upload) { + this.set( + path, + EmberObject.create({ + url: upload.url, + id: upload.id, + }) + ); + } + + @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.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 + 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 }, + ]; + } + + +} diff --git a/frontend/discourse/admin/components/edit-category-security-v2.gjs b/frontend/discourse/admin/components/edit-category-security-v2.gjs new file mode 100644 index 00000000000..89b56582427 --- /dev/null +++ b/frontend/discourse/admin/components/edit-category-security-v2.gjs @@ -0,0 +1,156 @@ +import { hash } from "@ember/helper"; +import { action } from "@ember/object"; +import { not } from "@ember/object/computed"; +import { service } from "@ember/service"; +import { buildCategoryPanel } from "discourse/admin/components/edit-category-panel"; +import CategoryPermissionRow from "discourse/components/category-permission-row"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import lazyHash from "discourse/helpers/lazy-hash"; +import { AUTO_GROUPS } from "discourse/lib/constants"; +import PermissionType from "discourse/models/permission-type"; +import ComboBox from "discourse/select-kit/components/combo-box"; +import { i18n } from "discourse-i18n"; + +export default class EditCategorySecurityV2 extends buildCategoryPanel( + "security" +) { + @service site; + + selectedGroup = null; + + @not("selectedGroup") noGroupSelected; + + get everyonePermission() { + return this.category.permissions.find( + (p) => p.group_id === AUTO_GROUPS.everyone.id + ); + } + + get everyoneGrantedFull() { + return ( + this.everyonePermission && + this.everyonePermission.permission_type === PermissionType.FULL + ); + } + + get minimumPermission() { + return this.everyonePermission + ? this.everyonePermission.permission_type + : PermissionType.READONLY; + } + + @action + onSelectGroup(group_name) { + const group = this.site.groups.find((g) => g.name === group_name); + this.category.addPermission({ + group_name, + group_id: group?.id, + permission_type: this.minimumPermission, + }); + } + + @action + onChangeEveryonePermission(everyonePermissionType) { + this.category.permissions.forEach((permission, idx) => { + if (permission.group_id === AUTO_GROUPS.everyone.id) { + return; + } + + if (everyonePermissionType < permission.permission_type) { + this.category.set( + `permissions.${idx}.permission_type`, + everyonePermissionType + ); + } + }); + } + + +} diff --git a/frontend/discourse/admin/components/edit-category-security.gjs b/frontend/discourse/admin/components/edit-category-security.gjs index 7bf7439b700..a79aba89a8f 100644 --- a/frontend/discourse/admin/components/edit-category-security.gjs +++ b/frontend/discourse/admin/components/edit-category-security.gjs @@ -134,9 +134,13 @@ export default class EditCategorySecurity extends buildCategoryPanel( {{/if}} {{#if this.everyoneGrantedFull}} -

{{i18n - "category.permissions.everyone_has_access" - }}

+ <@form.Alert @type="warning"> + {{i18n "category.permissions.everyone_has_access"}} + + {{else}} + <@form.Alert @type="warning"> + {{i18n "category.permissions.specific_groups_have_access"}} + {{/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); + } + + +} diff --git a/frontend/discourse/admin/components/edit-category-tab.gjs b/frontend/discourse/admin/components/edit-category-tab.gjs index d3f2f92dd2e..a2e438f4bda 100644 --- a/frontend/discourse/admin/components/edit-category-tab.gjs +++ b/frontend/discourse/admin/components/edit-category-tab.gjs @@ -34,15 +34,6 @@ export default class EditCategoryTab extends Component { scheduleOnce("afterRender", this, this._addToCollection); } - willDestroyElement() { - super.willDestroyElement(...arguments); - - this.setProperties({ - selectedTab: "general", - params: {}, - }); - } - _addToCollection() { addUniqueValueToArray(this.panels, this.tabClassName); } diff --git a/frontend/discourse/admin/components/edit-category-tags-v2.gjs b/frontend/discourse/admin/components/edit-category-tags-v2.gjs new file mode 100644 index 00000000000..bde564ad5c9 --- /dev/null +++ b/frontend/discourse/admin/components/edit-category-tags-v2.gjs @@ -0,0 +1,133 @@ +import { array, fn, hash } from "@ember/helper"; +import { on } from "@ember/modifier"; +import { action, set } from "@ember/object"; +import { and, empty } from "@ember/object/computed"; +import { LinkTo } from "@ember/routing"; +import { buildCategoryPanel } from "discourse/admin/components/edit-category-panel"; +import DButton from "discourse/components/d-button"; +import withEventValue from "discourse/helpers/with-event-value"; +import { removeValueFromArray } from "discourse/lib/array-tools"; +import TagChooser from "discourse/select-kit/components/tag-chooser"; +import TagGroupChooser from "discourse/select-kit/components/tag-group-chooser"; +import { i18n } from "discourse-i18n"; + +export default class EditCategoryTagsV2 extends buildCategoryPanel("tags") { + @empty("category.allowed_tags") allowedTagsEmpty; + @empty("category.allowed_tag_groups") allowedTagGroupsEmpty; + @and("allowedTagsEmpty", "allowedTagGroupsEmpty") disableAllowGlobalTags; + + @action + onTagGroupChange(rtg, valueArray) { + // A little strange, but we're using a multi-select component + // to select a single tag group. This action takes the array + // and extracts the first value in it. + set(rtg, "name", valueArray[0]); + } + + @action + addRequiredTagGroup() { + this.category.required_tag_groups.push({ + min_count: 1, + }); + } + + @action + deleteRequiredTagGroup(rtg) { + removeValueFromArray(this.category.required_tag_groups, rtg); + } + + +} diff --git a/frontend/discourse/admin/controllers/edit-category/tabs.js b/frontend/discourse/admin/controllers/edit-category/tabs.js index 5a853c4dce2..84e36908565 100644 --- a/frontend/discourse/admin/controllers/edit-category/tabs.js +++ b/frontend/discourse/admin/controllers/edit-category/tabs.js @@ -2,8 +2,9 @@ import { tracked } from "@glimmer/tracking"; import Controller from "@ember/controller"; import { action, getProperties } from "@ember/object"; import { and } from "@ember/object/computed"; +import { next } from "@ember/runloop"; import { service } from "@ember/service"; -import { underscore } from "@ember/string"; +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"; @@ -24,18 +25,66 @@ const FIELD_LIST = [ "emoji", "icon", "localizations", + "position", + "num_featured_topics", + "search_priority", + "allow_badges", + "topic_featured_link_allowed", + "navigate_to_first_post_after_read", + "all_topics_wiki", + "allow_unlimited_owner_edits_on_first_post", + "moderating_group_ids", + "auto_close_hours", + "auto_close_based_on_last_post", + "default_view", + "default_top_period", + "sort_order", + "sort_ascending", + "default_list_filter", + "show_subcategory_list", + "subcategory_list_style", + "read_only_banner", + "email_in", + "email_in_allow_strangers", + "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 { @service currentUser; @service dialog; @service site; @service router; + @service keyValueStore; @tracked breadcrumbCategories = this.site.get("categoriesList"); + @tracked + showAdvancedTabs = + this.keyValueStore.getItem(SHOW_ADVANCED_TABS_KEY) === "true"; + @tracked selectedTab = "general"; + @tracked previewData = new TrackedObject(PREVIEW_DEFAULTS); @trackedArray panels = []; - - selectedTab = "general"; saving = false; deleting = false; showTooltip = false; @@ -51,7 +100,7 @@ export default class EditCategoryTabsController extends Controller { const data = getProperties(this.model, ...FIELD_LIST); if (!this.model.styleType) { - data.style_type = "square"; + data.style_type = "icon"; } return data; @@ -63,6 +112,10 @@ export default class EditCategoryTabsController extends Controller { return false; } + if (transientData.style_type === "emoji" && !transientData.emoji) { + return false; + } + if (this.saving || this.deleting) { return true; } @@ -89,18 +142,36 @@ export default class EditCategoryTabsController extends Controller { return id ? "category.save" : "category.create"; } - @discourseComputed("model.id", "model.name") - title(id, name) { - return id - ? i18n("category.edit_dialog_title", { - categoryName: name, - }) - : i18n("category.create"); + get baseTitle() { + if (this.model.id) { + return i18n("category.edit_dialog_title", { + categoryName: this.model.name, + }); + } + + return i18n("category.create"); } - @discourseComputed("selectedTab") - selectedTabTitle(tab) { - return i18n(`category.${underscore(tab)}`); + @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; + this.showAdvancedTabs = this.showAdvancedTabs || tab !== "general"; } @action @@ -139,6 +210,11 @@ export default class EditCategoryTabsController extends Controller { this.model.setProperties(data); + // If permissions is empty or not set, ensure it's an empty array (public category) + if (!this.model.permissions || this.model.permissions.length === 0) { + this.model.set("permissions", []); + } + const lostAccess = this._wouldLoseAccess(); if (lostAccess) { @@ -214,4 +290,20 @@ export default class EditCategoryTabsController extends Controller { goBack() { DiscourseURL.routeTo(this.model.url); } + + @action + toggleAdvancedTabs() { + this.showAdvancedTabs = !this.showAdvancedTabs; + + // Save preference to localStorage + this.keyValueStore.setItem( + SHOW_ADVANCED_TABS_KEY, + this.showAdvancedTabs.toString() + ); + + // Always ensure we're on general tab after toggling + next(() => { + this.selectedTab = "general"; + }); + } } diff --git a/frontend/discourse/admin/routes/edit-category/tabs.js b/frontend/discourse/admin/routes/edit-category/tabs.js index 46c041e8034..5b48ec65ec0 100644 --- a/frontend/discourse/admin/routes/edit-category/tabs.js +++ b/frontend/discourse/admin/routes/edit-category/tabs.js @@ -9,11 +9,15 @@ export default class EditCategoryTabs extends DiscourseRoute { super.setupController(...arguments); const parentParams = this.paramsFor("editCategory"); + const selectedTab = transition.to.params.tab; controller.setProperties({ parentParams, - selectedTab: transition.to.params.tab, showTooltip: false, }); + + controller.setSelectedTab(selectedTab); + + controller.resetPreview(); } } diff --git a/frontend/discourse/admin/routes/new-category.js b/frontend/discourse/admin/routes/new-category.js index 9bbbb911647..7ca4b8a4462 100644 --- a/frontend/discourse/admin/routes/new-category.js +++ b/frontend/discourse/admin/routes/new-category.js @@ -49,6 +49,8 @@ export default class NewCategory extends DiscourseRoute { return this.store.createRecord("category", { color: backgroundColor, text_color: textColor, + style_type: "icon", + icon: "square-full", group_permissions, available_groups: this.site.groups.map((g) => g.name), allow_badges: true, @@ -63,6 +65,13 @@ export default class NewCategory extends DiscourseRoute { }); } + setupController(controller) { + super.setupController(...arguments); + + controller.resetPreview(); + controller.selectedTab = "general"; + } + titleToken() { return i18n("category.create"); } diff --git a/frontend/discourse/admin/templates/edit-category/tabs-horizontal.gjs b/frontend/discourse/admin/templates/edit-category/tabs-horizontal.gjs new file mode 100644 index 00000000000..8c7190dc831 --- /dev/null +++ b/frontend/discourse/admin/templates/edit-category/tabs-horizontal.gjs @@ -0,0 +1,97 @@ +import { concat } from "@ember/helper"; +import { on } from "@ember/modifier"; +import EditCategoryTab from "discourse/admin/components/edit-category-tab"; +import BreadCrumbs from "discourse/components/bread-crumbs"; +import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; +import DPageHeader from "discourse/components/d-page-header"; +import DToggleSwitch from "discourse/components/d-toggle-switch"; +import { and } from "discourse/truth-helpers"; +import { i18n } from "discourse-i18n"; + +const EditCategoryTabsHorizontalTemplate = ; + +export default EditCategoryTabsHorizontalTemplate; diff --git a/frontend/discourse/admin/templates/edit-category/tabs-vertical.gjs b/frontend/discourse/admin/templates/edit-category/tabs-vertical.gjs new file mode 100644 index 00000000000..0dd4633dcb8 --- /dev/null +++ b/frontend/discourse/admin/templates/edit-category/tabs-vertical.gjs @@ -0,0 +1,92 @@ +import EditCategoryTab from "discourse/admin/components/edit-category-tab"; +import BreadCrumbs from "discourse/components/bread-crumbs"; +import DButton from "discourse/components/d-button"; +import { and } from "discourse/truth-helpers"; + +const EditCategoryTabsVerticalTemplate = ; + +export default EditCategoryTabsVerticalTemplate; diff --git a/frontend/discourse/admin/templates/edit-category/tabs.gjs b/frontend/discourse/admin/templates/edit-category/tabs.gjs index bee11b0718e..2af24e5df95 100644 --- a/frontend/discourse/admin/templates/edit-category/tabs.gjs +++ b/frontend/discourse/admin/templates/edit-category/tabs.gjs @@ -1,16 +1,21 @@ +import { concat } from "@ember/helper"; import { htmlSafe } from "@ember/template"; import EditCategoryGeneral from "discourse/admin/components/edit-category-general"; +import EditCategoryGeneralV2 from "discourse/admin/components/edit-category-general-v2"; import EditCategoryImages from "discourse/admin/components/edit-category-images"; +import EditCategoryImagesV2 from "discourse/admin/components/edit-category-images-v2"; import EditCategoryLocalizations from "discourse/admin/components/edit-category-localizations"; import EditCategorySecurity from "discourse/admin/components/edit-category-security"; +import EditCategorySecurityV2 from "discourse/admin/components/edit-category-security-v2"; import EditCategorySettings from "discourse/admin/components/edit-category-settings"; -import EditCategoryTab from "discourse/admin/components/edit-category-tab"; +import EditCategorySettingsV2 from "discourse/admin/components/edit-category-settings-v2"; import EditCategoryTags from "discourse/admin/components/edit-category-tags"; +import EditCategoryTagsV2 from "discourse/admin/components/edit-category-tags-v2"; import EditCategoryTopicTemplate from "discourse/admin/components/edit-category-topic-template"; -import BreadCrumbs from "discourse/components/bread-crumbs"; -import DButton from "discourse/components/d-button"; +import EditCategoryTabsHorizontal from "discourse/admin/templates/edit-category/tabs-horizontal"; +import EditCategoryTabsVertical from "discourse/admin/templates/edit-category/tabs-vertical"; import Form from "discourse/components/form"; -import { and, not } from "discourse/truth-helpers"; +import { not } from "discourse/truth-helpers"; const TAB_COMPONENTS = { general: EditCategoryGeneral, @@ -22,91 +27,33 @@ const TAB_COMPONENTS = { localizations: EditCategoryLocalizations, }; -function componentFor(name) { - name = name.replace("edit-category-", ""); +const TAB_COMPONENTS_V2 = { + general: EditCategoryGeneralV2, + security: EditCategorySecurityV2, + settings: EditCategorySettingsV2, + images: EditCategoryImagesV2, + "topic-template": EditCategoryTopicTemplate, + tags: EditCategoryTagsV2, + localizations: EditCategoryLocalizations, +}; - if (!TAB_COMPONENTS[name]) { +function componentFor(name, useSimplified) { + name = name.replace("edit-category-", ""); + const components = useSimplified ? TAB_COMPONENTS_V2 : TAB_COMPONENTS; + + if (!components[name]) { throw new Error(`No category-tab component found for tab name: ${name}`); } - return TAB_COMPONENTS[name]; + return components[name]; } export default