mirror of
https://github.com/discourse/discourse.git
synced 2026-08-02 09:29:37 -05:00
FEATURE: Simplify category creation, hide settings (#36998)
This commit introduces a new simplified category creation and editing form, hidden behind a `enable_simplified_category_creation` upcoming change in the "conceptual" status right now. The simplified form allows users to toggle between simple + advanced mode: <img width="1106" height="641" alt="image" src="https://github.com/user-attachments/assets/2ff70db7-280b-4edf-a119-bbb6554a1408" /> <img width="1116" height="694" alt="image" src="https://github.com/user-attachments/assets/79e7e7d7-f7a8-49ee-80de-06f4dcb8a93f" /> The aim here is to make it faster for admins to create new categories, which can be a fairly convoluted process right now. Our future aim is to have different "types" of category creation flows. In addition, all other forms in the other category tabs have been converted to use FormKit. --------- Co-authored-by: Martin Brennan <martin@discourse.org> Co-authored-by: Régis Hanol <regis@hanol.fr>
This commit is contained in:
co-authored by
Martin Brennan
Régis Hanol
parent
6c0afb4a54
commit
af3e686989
@@ -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";
|
||||
|
||||
@@ -405,7 +405,6 @@ input {
|
||||
|
||||
&::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, <a href="%{settingLink}">please disable the setting here</a>. If you want to change the name or description, go to <a href="%{customizeLink}">Customize / Text Content</a>.'
|
||||
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 <a href='%{url}'>'fixed category positions'</a> 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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class={{this.panelClass}}>
|
||||
{{#if this.showWarning}}
|
||||
<@form.Alert @type="warning" @icon="triangle-exclamation">
|
||||
{{htmlSafe
|
||||
(i18n
|
||||
"category.uncategorized_general_warning"
|
||||
settingLink=this.uncategorizedSiteSettingLink
|
||||
customizeLink=this.customizeTextContentLink
|
||||
)
|
||||
}}
|
||||
</@form.Alert>
|
||||
{{/if}}
|
||||
|
||||
{{#unless @category.isUncategorizedCategory}}
|
||||
<@form.Field
|
||||
@name="name"
|
||||
@title={{i18n "category.name"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
@onSet={{this.onNameChange}}
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n "category.name_placeholder"}}
|
||||
@maxlength="50"
|
||||
class="category-name"
|
||||
data-1p-ignore
|
||||
/>
|
||||
</@form.Field>
|
||||
{{/unless}}
|
||||
|
||||
<@form.Field
|
||||
@name="color"
|
||||
@title={{i18n "category.background_color"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
@onSet={{this.onBackgroundColorSet}}
|
||||
as |field|
|
||||
>
|
||||
<field.Color
|
||||
@colors={{this.backgroundColors}}
|
||||
@usedColors={{this.usedBackgroundColors}}
|
||||
@collapseSwatches={{true}}
|
||||
@collapseSwatchesLabel={{i18n "category.color_palette"}}
|
||||
/>
|
||||
</@form.Field>
|
||||
|
||||
<@form.Container @title={{i18n "category.style"}}>
|
||||
<@form.ConditionalContent
|
||||
@activeName={{or @category.styleType "square"}}
|
||||
@onChange={{this.onStyleTypeChange}}
|
||||
as |cc|
|
||||
>
|
||||
<cc.Conditions as |Condition|>
|
||||
<Condition @name="icon">
|
||||
{{i18n "category.styles.icon"}}
|
||||
</Condition>
|
||||
<Condition @name="emoji">
|
||||
{{i18n "category.styles.emoji"}}
|
||||
</Condition>
|
||||
<Condition @name="square">
|
||||
{{i18n "category.styles.square"}}
|
||||
</Condition>
|
||||
</cc.Conditions>
|
||||
|
||||
<cc.Contents as |Content|>
|
||||
<Content @name="icon">
|
||||
<@form.Field
|
||||
@name="icon"
|
||||
@title={{i18n "category.icon"}}
|
||||
@showTitle={{false}}
|
||||
@format="large"
|
||||
@onSet={{this.onIconSet}}
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<IconPicker
|
||||
@value={{readonly field.value}}
|
||||
@onlyAvailable={{true}}
|
||||
@options={{hash
|
||||
maximum=1
|
||||
disabled=field.disabled
|
||||
caretDownIcon="angle-down"
|
||||
caretUpIcon="angle-up"
|
||||
icons=field.value
|
||||
}}
|
||||
@onChange={{field.set}}
|
||||
class="form-kit__control-icon"
|
||||
style={{htmlSafe
|
||||
(concat "--icon-color: #" @transientData.color ";")
|
||||
}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
</Content>
|
||||
|
||||
<Content @name="emoji">
|
||||
<@form.Field
|
||||
@name="emoji"
|
||||
@title={{i18n "category.emoji"}}
|
||||
@showTitle={{false}}
|
||||
@format="large"
|
||||
@onSet={{this.onEmojiSet}}
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<EmojiPicker
|
||||
@emoji={{field.value}}
|
||||
@didSelectEmoji={{field.set}}
|
||||
@modalForMobile={{false}}
|
||||
@btnClass="btn-default btn-emoji"
|
||||
@label={{unless field.value (i18n "category.select_emoji")}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
</Content>
|
||||
</cc.Contents>
|
||||
</@form.ConditionalContent>
|
||||
</@form.Container>
|
||||
|
||||
{{#unless @category.isUncategorizedCategory}}
|
||||
<@form.Field
|
||||
@name="parent_category_id"
|
||||
@title={{i18n "category.subcategory_of"}}
|
||||
@format="large"
|
||||
@onSet={{this.onParentCategorySet}}
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<CategoryChooser
|
||||
@value={{@transientData.parent_category_id}}
|
||||
@onChange={{field.set}}
|
||||
@options={{hash
|
||||
allowSubCategories=this.allowSubCategoriesAsParent
|
||||
allowRestrictedCategories=true
|
||||
allowUncategorized=false
|
||||
excludeCategoryId=@category.id
|
||||
none="category.none_subcategory_text"
|
||||
clearable=true
|
||||
caretUpIcon="chevron-up"
|
||||
caretDownIcon="chevron-down"
|
||||
}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
{{/unless}}
|
||||
|
||||
<@form.Container
|
||||
@title={{i18n "category.visibility.title"}}
|
||||
class="--radio-cards"
|
||||
>
|
||||
<@form.ConditionalContent
|
||||
@activeName={{this.categoryVisibility}}
|
||||
@onChange={{this.onVisibilityChange}}
|
||||
as |cc|
|
||||
>
|
||||
<cc.Conditions as |Condition|>
|
||||
{{#if this.parentIsRestricted}}
|
||||
<DTooltip
|
||||
@content={{i18n "category.subcategory_permissions_warning"}}
|
||||
>
|
||||
<:trigger>
|
||||
<Condition @name="public" @disabled={{true}}>
|
||||
{{icon "ban"}}
|
||||
{{i18n this.publicVisibilityLabel}}
|
||||
</Condition>
|
||||
</:trigger>
|
||||
</DTooltip>
|
||||
{{else}}
|
||||
<Condition @name="public">
|
||||
{{icon "check"}}
|
||||
{{i18n this.publicVisibilityLabel}}
|
||||
</Condition>
|
||||
{{/if}}
|
||||
<Condition @name="group_restricted">
|
||||
{{icon "check"}}
|
||||
{{i18n "category.visibility.group_restricted"}}
|
||||
</Condition>
|
||||
</cc.Conditions>
|
||||
|
||||
<cc.Contents as |Content|>
|
||||
<Content @name="group_restricted">
|
||||
<@form.Container
|
||||
@title={{i18n "category.visibility.who_can_see"}}
|
||||
>
|
||||
<GroupChooser
|
||||
@content={{@category.site.groups}}
|
||||
@value={{this.visibilityGroups}}
|
||||
@onChange={{this.onChangeVisibilityGroups}}
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container
|
||||
@title={{i18n "category.visibility.who_can_post"}}
|
||||
>
|
||||
<GroupChooser
|
||||
@content={{@category.site.groups}}
|
||||
@value={{this.postingGroups}}
|
||||
@onChange={{this.onChangePostingGroups}}
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
<span class="category-permission-hint">
|
||||
{{i18n "category.visibility.more_options_hint"}}
|
||||
<a href {{on "click" this.goToSecurityTab}}>
|
||||
{{i18n "category.visibility.more_options_hint_link"}}
|
||||
</a>
|
||||
</span>
|
||||
</Content>
|
||||
</cc.Contents>
|
||||
</@form.ConditionalContent>
|
||||
</@form.Container>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -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 },
|
||||
];
|
||||
}
|
||||
|
||||
<template>
|
||||
<@form.Container
|
||||
@title={{i18n "category.logo"}}
|
||||
@subtitle={{i18n "category.logo_description"}}
|
||||
>
|
||||
<UppyImageUploader
|
||||
@imageUrl={{this.logoImageUrl}}
|
||||
@onUploadDone={{this.logoUploadDone}}
|
||||
@onUploadDeleted={{this.logoUploadDeleted}}
|
||||
@type="category_logo"
|
||||
@id="category-logo-uploader"
|
||||
class="no-repeat contain-image"
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container
|
||||
@title={{i18n "category.logo_dark"}}
|
||||
@subtitle={{i18n "category.logo_description"}}
|
||||
>
|
||||
<UppyImageUploader
|
||||
@imageUrl={{this.logoImageDarkUrl}}
|
||||
@onUploadDone={{this.logoDarkUploadDone}}
|
||||
@onUploadDeleted={{this.logoDarkUploadDeleted}}
|
||||
@type="category_logo_dark"
|
||||
@id="category-dark-logo-uploader"
|
||||
class="no-repeat contain-image"
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container @title={{i18n "category.background_image"}}>
|
||||
<UppyImageUploader
|
||||
@imageUrl={{this.backgroundImageUrl}}
|
||||
@onUploadDone={{this.backgroundUploadDone}}
|
||||
@onUploadDeleted={{this.backgroundUploadDeleted}}
|
||||
@type="category_background"
|
||||
@id="category-background-uploader"
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container @title={{i18n "category.background_image_dark"}}>
|
||||
<UppyImageUploader
|
||||
@imageUrl={{this.backgroundDarkImageUrl}}
|
||||
@onUploadDone={{this.backgroundDarkUploadDone}}
|
||||
@onUploadDeleted={{this.backgroundDarkUploadDeleted}}
|
||||
@type="category_background_dark"
|
||||
@id="category-dark-background-uploader"
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
{{! This field is removed from edit-category-general when the UC is active }}
|
||||
{{#if this.siteSettings.enable_simplified_category_creation}}
|
||||
<@form.Field
|
||||
@name="text_color"
|
||||
@title={{i18n "category.foreground_color"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Color @colors={{CATEGORY_TEXT_COLORS}} />
|
||||
</@form.Field>
|
||||
{{/if}}
|
||||
|
||||
<@form.Field
|
||||
@name="default_view"
|
||||
@title={{i18n "category.default_view"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@id="category-default-view"
|
||||
@content={{this.availableViews}}
|
||||
@value={{field.value}}
|
||||
@onChange={{field.set}}
|
||||
@options={{hash placementStrategy="absolute"}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
|
||||
<@form.Field
|
||||
@name="default_top_period"
|
||||
@title={{i18n "category.default_top_period"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@id="category-default-period"
|
||||
@content={{this.availableTopPeriods}}
|
||||
@value={{field.value}}
|
||||
@onChange={{field.set}}
|
||||
@options={{hash placementStrategy="absolute"}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
|
||||
<@form.Field
|
||||
@name="sort_order"
|
||||
@title={{i18n "category.sort_order"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@content={{this.availableSorts}}
|
||||
@value={{field.value}}
|
||||
@options={{hash none="category.sort_options.default"}}
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
{{#unless this.isDefaultSortOrder}}
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@content={{this.sortAscendingOptions}}
|
||||
@value={{this.sortAscendingOption}}
|
||||
@options={{hash
|
||||
none="category.sort_options.default"
|
||||
placementStrategy="absolute"
|
||||
}}
|
||||
@onChange={{fn (mut @category.sort_ascending)}}
|
||||
/>
|
||||
{{/unless}}
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
|
||||
<@form.Field
|
||||
@name="default_list_filter"
|
||||
@title={{i18n "category.default_list_filter"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<ComboBox
|
||||
@id="category-default-filter"
|
||||
@valueProperty="value"
|
||||
@content={{this.availableListFilters}}
|
||||
@value={{field.value}}
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
|
||||
{{#if this.isParentCategory}}
|
||||
<@form.Field
|
||||
@name="show_subcategory_list"
|
||||
@title={{i18n "category.show_subcategory_list"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
|
||||
{{#if @transientData.show_subcategory_list}}
|
||||
<@form.Field
|
||||
@name="subcategory_list_style"
|
||||
@title={{i18n "category.subcategory_list_style"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@id="subcategory-list-style"
|
||||
@content={{this.availableSubcategoryListStyles}}
|
||||
@value={{field.value}}
|
||||
@onChange={{field.set}}
|
||||
@options={{hash placementStrategy="absolute"}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<@form.Field
|
||||
@name="read_only_banner"
|
||||
@title={{i18n "category.read_only_banner"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input @maxlength="255" />
|
||||
</@form.Field>
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-custom-images"
|
||||
@outletArgs={{lazyHash category=this.category}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.category.is_special}}
|
||||
{{#if this.category.isUncategorizedCategory}}
|
||||
<@form.Alert @type="warning">
|
||||
{{i18n "category.uncategorized_security_warning"}}
|
||||
</@form.Alert>
|
||||
{{else}}
|
||||
<@form.Alert @type="warning">
|
||||
{{i18n "category.special_warning"}}
|
||||
</@form.Alert>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.category.is_special}}
|
||||
<@form.Container>
|
||||
<div class="category-permissions-table">
|
||||
<div class="permission-row row-header">
|
||||
<span class="group-name">{{i18n
|
||||
"category.permissions.group"
|
||||
}}</span>
|
||||
<span class="options">
|
||||
<span class="cell">{{i18n "category.permissions.see"}}</span>
|
||||
<span class="cell">{{i18n "category.permissions.reply"}}</span>
|
||||
<span class="cell">{{i18n "category.permissions.create"}}</span>
|
||||
<span class="cell"></span>
|
||||
</span>
|
||||
</div>
|
||||
{{#each this.category.permissions as |p|}}
|
||||
<CategoryPermissionRow
|
||||
@groupId={{p.group_id}}
|
||||
@groupName={{p.group_name}}
|
||||
@type={{p.permission_type}}
|
||||
@category={{this.category}}
|
||||
@everyonePermission={{this.everyonePermission}}
|
||||
@onChangeEveryonePermission={{this.onChangeEveryonePermission}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
{{#unless this.category.permissions}}
|
||||
<div class="permission-row row-empty">
|
||||
{{i18n "category.permissions.no_groups_selected"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if this.category.availableGroups}}
|
||||
<PluginOutlet
|
||||
@name="category-security-permissions-add-group"
|
||||
@outletArgs={{lazyHash
|
||||
category=this.category
|
||||
availableGroups=this.category.availableGroups
|
||||
onSelectGroup=this.onSelectGroup
|
||||
}}
|
||||
@defaultGlimmer={{true}}
|
||||
>
|
||||
<div class="add-group">
|
||||
<span class="group-name">
|
||||
<ComboBox
|
||||
@content={{this.category.availableGroups}}
|
||||
@onChange={{this.onSelectGroup}}
|
||||
@value={{null}}
|
||||
@valueProperty={{null}}
|
||||
@nameProperty={{null}}
|
||||
@options={{hash none="category.security_add_group"}}
|
||||
class="available-groups"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</PluginOutlet>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.everyoneGrantedFull}}
|
||||
<@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}}
|
||||
</@form.Container>
|
||||
{{/unless}}
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-custom-security"
|
||||
@outletArgs={{lazyHash category=this.category}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -134,9 +134,13 @@ export default class EditCategorySecurity extends buildCategoryPanel(
|
||||
{{/if}}
|
||||
|
||||
{{#if this.everyoneGrantedFull}}
|
||||
<p class="warning everyone-has-access-warning">{{i18n
|
||||
"category.permissions.everyone_has_access"
|
||||
}}</p>
|
||||
<@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}}
|
||||
</PluginOutlet>
|
||||
{{/unless}}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{! 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|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n "category.slug_placeholder"}}
|
||||
@maxlength="255"
|
||||
/>
|
||||
</@form.Field>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showPositionInput}}
|
||||
<@form.Field
|
||||
@name="position"
|
||||
@title={{i18n "category.position"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input type="number" min="0" />
|
||||
</@form.Field>
|
||||
{{/if}}
|
||||
|
||||
<@form.Field
|
||||
@name="num_featured_topics"
|
||||
@title={{if
|
||||
@category.parent_category_id
|
||||
(i18n "category.subcategory_num_featured_topics")
|
||||
(i18n "category.num_featured_topics")
|
||||
}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input type="number" min="1" />
|
||||
</@form.Field>
|
||||
|
||||
<@form.Field
|
||||
@name="search_priority"
|
||||
@title={{i18n "category.search_priority.label"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@id="category-search-priority"
|
||||
@content={{this.searchPrioritiesOptions}}
|
||||
@value={{field.value}}
|
||||
@onChange={{field.set}}
|
||||
@options={{hash placementStrategy="absolute"}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
|
||||
{{#if this.siteSettings.enable_badges}}
|
||||
<@form.Field
|
||||
@name="allow_badges"
|
||||
@title={{i18n "category.allow_badges_label"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.siteSettings.topic_featured_link_enabled}}
|
||||
<@form.Field
|
||||
@name="topic_featured_link_allowed"
|
||||
@title={{i18n "category.topic_featured_link_allowed"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
{{/if}}
|
||||
|
||||
<@form.Field
|
||||
@name="navigate_to_first_post_after_read"
|
||||
@title={{i18n "category.navigate_to_first_post_after_read"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
|
||||
<@form.Field
|
||||
@name="all_topics_wiki"
|
||||
@title={{i18n "category.all_topics_wiki"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
|
||||
<@form.Field
|
||||
@name="allow_unlimited_owner_edits_on_first_post"
|
||||
@title={{i18n "category.allow_unlimited_owner_edits_on_first_post"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
|
||||
<@form.Section @title={{i18n "category.settings_sections.moderation"}}>
|
||||
{{#if this.siteSettings.enable_category_group_moderation}}
|
||||
<@form.Container @title={{i18n "category.reviewable_by_group"}}>
|
||||
<GroupChooser
|
||||
@content={{this.site.groups}}
|
||||
@value={{@category.moderating_group_ids}}
|
||||
@onChange={{this.onCategoryModeratingGroupsChange}}
|
||||
/>
|
||||
</@form.Container>
|
||||
{{/if}}
|
||||
|
||||
<@form.Container>
|
||||
<label class="checkbox-label">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{@category.category_setting.require_topic_approval}}
|
||||
/>
|
||||
{{i18n "category.require_topic_approval"}}
|
||||
</label>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container>
|
||||
<label class="checkbox-label">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{@category.category_setting.require_reply_approval}}
|
||||
/>
|
||||
{{i18n "category.require_reply_approval"}}
|
||||
</label>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container @title={{i18n "category.default_slow_mode"}}>
|
||||
<RelativeTimePicker
|
||||
@id="category-default-slow-mode"
|
||||
@durationMinutes={{@category.defaultSlowModeMinutes}}
|
||||
@onChange={{this.onDefaultSlowModeDurationChange}}
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container @title={{i18n "topic.auto_close.label"}}>
|
||||
<RelativeTimePicker
|
||||
@id="topic-auto-close"
|
||||
@durationHours={{@category.auto_close_hours}}
|
||||
@hiddenIntervals={{this.hiddenRelativeIntervals}}
|
||||
@onChange={{this.onAutoCloseDurationChange}}
|
||||
/>
|
||||
<label class="checkbox-label">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{@category.auto_close_based_on_last_post}}
|
||||
/>
|
||||
{{i18n "topic.auto_close.based_on_last_post"}}
|
||||
</label>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container @title={{i18n "category.num_auto_bump_daily"}}>
|
||||
<input
|
||||
{{on
|
||||
"input"
|
||||
(withEventValue
|
||||
(fn (mut @category.category_setting.num_auto_bump_daily))
|
||||
)
|
||||
}}
|
||||
value={{@category.category_setting.num_auto_bump_daily}}
|
||||
type="number"
|
||||
min="0"
|
||||
id="category-number-daily-bump"
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container @title={{i18n "category.auto_bump_cooldown_days"}}>
|
||||
<input
|
||||
{{on
|
||||
"input"
|
||||
(withEventValue
|
||||
(fn (mut @category.category_setting.auto_bump_cooldown_days))
|
||||
)
|
||||
}}
|
||||
value={{@category.category_setting.auto_bump_cooldown_days}}
|
||||
type="number"
|
||||
min="0"
|
||||
id="category-auto-bump-cooldown-days"
|
||||
/>
|
||||
</@form.Container>
|
||||
</@form.Section>
|
||||
|
||||
<@form.Section @title={{i18n "category.settings_sections.email"}}>
|
||||
{{#if this.emailInEnabled}}
|
||||
<@form.Field
|
||||
@name="email_in"
|
||||
@title={{i18n "category.email_in"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input @maxlength="255" />
|
||||
</@form.Field>
|
||||
|
||||
<@form.Field
|
||||
@name="email_in_allow_strangers"
|
||||
@title={{i18n "category.email_in_allow_strangers"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
|
||||
<@form.Field
|
||||
@name="mailinglist_mirror"
|
||||
@title={{i18n "category.mailinglist_mirror"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-email-in"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{lazyHash category=@category}}
|
||||
/>
|
||||
{{else}}
|
||||
<@form.Alert @type="info">
|
||||
{{htmlSafe
|
||||
(i18n
|
||||
"category.email_in_disabled"
|
||||
setting_url=(getUrl
|
||||
"/admin/site_settings/category/all_results?filter=email_in"
|
||||
)
|
||||
)
|
||||
}}
|
||||
</@form.Alert>
|
||||
{{/if}}
|
||||
</@form.Section>
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-custom-settings"
|
||||
@outletArgs={{lazyHash category=@category}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
<template>
|
||||
<@form.Field
|
||||
@name="minimum_required_tags"
|
||||
@title={{i18n "category.minimum_required_tags"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input type="number" min="0" id="category-minimum-tags" />
|
||||
</@form.Field>
|
||||
|
||||
<@form.Container
|
||||
@title={{if
|
||||
this.category.id
|
||||
(i18n "category.tags_allowed_tags" categoryName=this.category.name)
|
||||
(i18n "category.tags_allowed_tags_new_category")
|
||||
}}
|
||||
>
|
||||
<TagChooser
|
||||
@id="category-allowed-tags"
|
||||
@tags={{this.category.allowed_tags}}
|
||||
@everyTag={{true}}
|
||||
@excludeSynonyms={{true}}
|
||||
@unlimitedTagCount={{true}}
|
||||
@onChange={{fn (mut this.category.allowed_tags)}}
|
||||
@options={{hash filterPlaceholder="category.tags_placeholder"}}
|
||||
/>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Container
|
||||
@title={{if
|
||||
this.category.id
|
||||
(i18n
|
||||
"category.tags_allowed_tag_groups" categoryName=this.category.name
|
||||
)
|
||||
(i18n "category.tags_allowed_tag_groups_new_category")
|
||||
}}
|
||||
>
|
||||
<TagGroupChooser
|
||||
@id="category-allowed-tag-groups"
|
||||
@tagGroups={{this.category.allowed_tag_groups}}
|
||||
@onChange={{fn (mut this.category.allowed_tag_groups)}}
|
||||
/>
|
||||
<LinkTo @route="tagGroups" class="manage-tag-groups">{{i18n
|
||||
"category.manage_tag_groups_link"
|
||||
}}</LinkTo>
|
||||
</@form.Container>
|
||||
|
||||
<@form.Field
|
||||
@name="allow_global_tags"
|
||||
@title={{i18n "category.allow_global_tags_label"}}
|
||||
@format="large"
|
||||
@disabled={{this.disableAllowGlobalTags}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</@form.Field>
|
||||
|
||||
<@form.Alert @type="info">
|
||||
{{i18n "category.tags_tab_description"}}
|
||||
</@form.Alert>
|
||||
|
||||
<@form.Section @title={{i18n "category.required_tag_group.description"}}>
|
||||
{{#each this.category.required_tag_groups as |rtg|}}
|
||||
<div class="required-tag-group-row">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={{rtg.min_count}}
|
||||
{{on "input" (withEventValue (fn (mut rtg.min_count)))}}
|
||||
/>
|
||||
<TagGroupChooser
|
||||
@tagGroups={{if rtg.name (array rtg.name) (array)}}
|
||||
@onChange={{fn this.onTagGroupChange rtg}}
|
||||
@options={{hash
|
||||
maximum=1
|
||||
filterPlaceholder="category.required_tag_group.placeholder"
|
||||
}}
|
||||
/>
|
||||
<DButton
|
||||
@label="category.required_tag_group.delete"
|
||||
@action={{fn this.deleteRequiredTagGroup rtg}}
|
||||
@icon="trash-can"
|
||||
class="delete-required-tag-group"
|
||||
/>
|
||||
</div>
|
||||
{{/each}}
|
||||
<DButton
|
||||
@label="category.required_tag_group.add"
|
||||
@action={{this.addRequiredTagGroup}}
|
||||
@icon="plus"
|
||||
class="btn-default add-required-tag-group"
|
||||
/>
|
||||
</@form.Section>
|
||||
</template>
|
||||
}
|
||||
@@ -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";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 = <template>
|
||||
<DPageHeader
|
||||
@titleLabel={{@controller.baseTitle}}
|
||||
@showDrawer={{true}}
|
||||
@collapseActionsOnMobile={{false}}
|
||||
>
|
||||
<:breadcrumbs>
|
||||
{{#if (and @controller.site.desktopView @controller.model.id)}}
|
||||
<DBreadcrumbsItem
|
||||
@path={{concat "/c/" @controller.model.slug}}
|
||||
@label={{i18n "category.back"}}
|
||||
/>
|
||||
{{/if}}
|
||||
</:breadcrumbs>
|
||||
<:actions>
|
||||
<DToggleSwitch
|
||||
@label="category.show_advanced"
|
||||
@state={{@controller.showAdvancedTabs}}
|
||||
{{on "click" @controller.toggleAdvancedTabs}}
|
||||
/>
|
||||
</:actions>
|
||||
<:tabs>
|
||||
{{#if @controller.showAdvancedTabs}}
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="general"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="security"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="settings"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="images"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="topic-template"
|
||||
/>
|
||||
{{#if @controller.siteSettings.tagging_enabled}}
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="tags"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if @controller.siteSettings.content_localization_enabled}}
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="localizations"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</:tabs>
|
||||
<:drawer>
|
||||
{{#if @controller.model.id}}
|
||||
<BreadCrumbs
|
||||
@categories={{@controller.breadcrumbCategories}}
|
||||
@category={{@controller.model}}
|
||||
@noSubcategories={{@controller.model.noSubcategories}}
|
||||
@editingCategory={{true}}
|
||||
@editingCategoryTab={{@controller.selectedTab}}
|
||||
/>
|
||||
{{/if}}
|
||||
</:drawer>
|
||||
</DPageHeader>
|
||||
</template>;
|
||||
|
||||
export default EditCategoryTabsHorizontalTemplate;
|
||||
@@ -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 = <template>
|
||||
<div class="edit-category-title-bar">
|
||||
<div class="edit-category-title">
|
||||
<h2 class="edit-category-title-text">
|
||||
<span
|
||||
class="edit-category-static-title"
|
||||
>{{@controller.baseTitle}}</span>
|
||||
<span class="edit-category-preview-badge">
|
||||
{{#if @controller.showPreviewBadge}}
|
||||
{{@controller.previewBadge}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</h2>
|
||||
{{#if @controller.model.id}}
|
||||
<BreadCrumbs
|
||||
@categories={{@controller.breadcrumbCategories}}
|
||||
@category={{@controller.model}}
|
||||
@noSubcategories={{@controller.model.noSubcategories}}
|
||||
@editingCategory={{true}}
|
||||
@editingCategoryTab={{@controller.selectedTab}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if (and @controller.site.desktopView @controller.model.id)}}
|
||||
<DButton
|
||||
@action={{@controller.goBack}}
|
||||
@label="category.back"
|
||||
@icon="caret-left"
|
||||
class="category-back"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="edit-category-nav">
|
||||
<ul class="nav nav-stacked">
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="general"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="security"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="settings"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="images"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="topic-template"
|
||||
/>
|
||||
{{#if @controller.siteSettings.tagging_enabled}}
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="tags"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if @controller.siteSettings.content_localization_enabled}}
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="localizations"
|
||||
/>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
export default EditCategoryTabsVerticalTemplate;
|
||||
@@ -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 <template>
|
||||
<div class="edit-category {{if @controller.expandedMenu 'expanded-menu'}}">
|
||||
<div class="edit-category-title-bar">
|
||||
<div class="edit-category-title">
|
||||
<h2>{{@controller.title}}</h2>
|
||||
{{#if @controller.model.id}}
|
||||
<BreadCrumbs
|
||||
@categories={{@controller.breadcrumbCategories}}
|
||||
@category={{@controller.model}}
|
||||
@noSubcategories={{@controller.model.noSubcategories}}
|
||||
@editingCategory={{true}}
|
||||
@editingCategoryTab={{@controller.selectedTab}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if (and @controller.site.desktopView @controller.model.id)}}
|
||||
<DButton
|
||||
@action={{@controller.goBack}}
|
||||
@label="category.back"
|
||||
@icon="caret-left"
|
||||
class="category-back"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="edit-category-nav">
|
||||
<ul class="nav nav-stacked">
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="general"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="security"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="settings"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="images"
|
||||
/>
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="topic-template"
|
||||
/>
|
||||
{{#if @controller.siteSettings.tagging_enabled}}
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="tags"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if @controller.siteSettings.content_localization_enabled}}
|
||||
<EditCategoryTab
|
||||
@panels={{@controller.panels}}
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@params={{@controller.parentParams}}
|
||||
@tab="localizations"
|
||||
/>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
{{#if @controller.siteSettings.enable_simplified_category_creation}}
|
||||
<EditCategoryTabsHorizontal @controller={{@controller}} />
|
||||
{{else}}
|
||||
<EditCategoryTabsVertical @controller={{@controller}} />
|
||||
{{/if}}
|
||||
|
||||
<Form
|
||||
@data={{@controller.formData}}
|
||||
@@ -115,20 +62,46 @@ export default <template>
|
||||
as |form transientData|
|
||||
>
|
||||
<form.Section
|
||||
@title={{@controller.selectedTabTitle}}
|
||||
class="edit-category-content"
|
||||
class="edit-category-content edit-category-tab-{{@controller.selectedTab}}"
|
||||
>
|
||||
{{#each @controller.panels as |tabName|}}
|
||||
{{#let (componentFor tabName) as |Tab|}}
|
||||
{{#if @controller.siteSettings.enable_simplified_category_creation}}
|
||||
|
||||
{{#let
|
||||
(componentFor
|
||||
(concat "edit-category-" @controller.selectedTab)
|
||||
@controller.siteSettings.enable_simplified_category_creation
|
||||
)
|
||||
as |Tab|
|
||||
}}
|
||||
<Tab
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@category={{@controller.model}}
|
||||
@registerValidator={{@controller.registerValidator}}
|
||||
@transientData={{transientData}}
|
||||
@form={{form}}
|
||||
@updatePreview={{@controller.updatePreview}}
|
||||
@setSelectedTab={{@controller.setSelectedTab}}
|
||||
/>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#each @controller.panels as |tabName|}}
|
||||
{{#let
|
||||
(componentFor
|
||||
tabName
|
||||
@controller.siteSettings.enable_simplified_category_creation
|
||||
)
|
||||
as |Tab|
|
||||
}}
|
||||
<Tab
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@category={{@controller.model}}
|
||||
@registerValidator={{@controller.registerValidator}}
|
||||
@transientData={{transientData}}
|
||||
@form={{form}}
|
||||
/>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</form.Section>
|
||||
|
||||
{{#if @controller.showDeleteReason}}
|
||||
|
||||
@@ -59,6 +59,10 @@ export default class DPageHeader extends Component {
|
||||
!HEADLESS_ACTIONS.find((segment) => pathSegments.includes(segment));
|
||||
}
|
||||
|
||||
get shouldCollapseActionsOnMobile() {
|
||||
return this.site.mobileView && this.args.collapseActionsOnMobile !== false;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.shouldDisplay}}
|
||||
<div class="d-page-header">
|
||||
@@ -77,7 +81,7 @@ export default class DPageHeader extends Component {
|
||||
|
||||
{{#if (or (has-block "actions") @headerActionComponent)}}
|
||||
<div class="d-page-header__actions">
|
||||
{{#if this.site.mobileView}}
|
||||
{{#if this.shouldCollapseActionsOnMobile}}
|
||||
<DMenu
|
||||
@identifier="d-page-header-mobile-actions"
|
||||
@title={{i18n "more_options"}}
|
||||
@@ -137,6 +141,12 @@ export default class DPageHeader extends Component {
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if @showDrawer}}
|
||||
<div class="d-page-header__drawer">
|
||||
{{yield to="drawer"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless @hideTabs}}
|
||||
<div class="d-nav-submenu">
|
||||
<HorizontalOverflowNav class="d-nav-submenu__tabs">
|
||||
|
||||
@@ -111,6 +111,8 @@ export default class FKControlWrapper extends Component {
|
||||
@colors={{@colors}}
|
||||
@usedColors={{@usedColors}}
|
||||
@allowNamedColors={{@allowNamedColors}}
|
||||
@collapseSwatches={{@collapseSwatches}}
|
||||
@collapseSwatchesLabel={{@collapseSwatchesLabel}}
|
||||
@onControlWidthChange={{fn (mut this.controlWidth)}}
|
||||
id={{@field.id}}
|
||||
name={{@field.name}}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import DMenu from "discourse/float-kit/components/d-menu";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
normalizeHex,
|
||||
resolveColor,
|
||||
} from "discourse/lib/color-transformations";
|
||||
import { and } from "discourse/truth-helpers";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
function isColorUsed(usedColors, color) {
|
||||
@@ -28,6 +30,34 @@ function colorLabel(usedColors, color) {
|
||||
return `#${color}${usedText}`;
|
||||
}
|
||||
|
||||
function calculateLuminance(hex) {
|
||||
if (!hex || hex.length < 3) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let normalized = hex.toUpperCase();
|
||||
|
||||
if (normalized.length === 3) {
|
||||
normalized =
|
||||
normalized[0] +
|
||||
normalized[0] +
|
||||
normalized[1] +
|
||||
normalized[1] +
|
||||
normalized[2] +
|
||||
normalized[2];
|
||||
}
|
||||
|
||||
const r = parseInt(normalized.slice(0, 2), 16);
|
||||
const g = parseInt(normalized.slice(2, 4), 16);
|
||||
const b = parseInt(normalized.slice(4, 6), 16);
|
||||
|
||||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
}
|
||||
|
||||
function colorLuminanceClass(color) {
|
||||
return calculateLuminance(color) > 0.5 ? "--is-light" : "--is-dark";
|
||||
}
|
||||
|
||||
export default class FKControlColor extends Component {
|
||||
static controlType = "color";
|
||||
|
||||
@@ -56,6 +86,16 @@ export default class FKControlColor extends Component {
|
||||
return "#000000";
|
||||
}
|
||||
|
||||
get pickerIconClass() {
|
||||
const value = this.args.field.value;
|
||||
if (!value || !isValidHex(value)) {
|
||||
return "--is-light";
|
||||
}
|
||||
|
||||
const hex = normalizeHex(value);
|
||||
return calculateLuminance(hex) > 0.5 ? "--is-light" : "--is-dark";
|
||||
}
|
||||
|
||||
@action
|
||||
handleTextInput(event) {
|
||||
this.args.field.set(event.target.value);
|
||||
@@ -74,8 +114,11 @@ export default class FKControlColor extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
selectColor(color) {
|
||||
selectColor(color, closeMenu) {
|
||||
this.args.field.set(color);
|
||||
if (typeof closeMenu === "function") {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
@@ -94,40 +137,87 @@ export default class FKControlColor extends Component {
|
||||
{{on "paste" this.handlePaste}}
|
||||
...attributes
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
value={{this.normalizedValueForPicker}}
|
||||
class="form-kit__control-color-input-picker"
|
||||
disabled={{@field.disabled}}
|
||||
{{on "input" this.handlePickerInput}}
|
||||
/>
|
||||
<span
|
||||
class={{concatClass
|
||||
"form-kit__control-color-picker-wrapper"
|
||||
this.pickerIconClass
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
value={{this.normalizedValueForPicker}}
|
||||
class="form-kit__control-color-input-picker"
|
||||
disabled={{@field.disabled}}
|
||||
{{on "input" this.handlePickerInput}}
|
||||
/>
|
||||
{{icon "eye-dropper"}}
|
||||
</span>
|
||||
{{#if (and @colors @collapseSwatches)}}
|
||||
<DMenu
|
||||
@identifier="color-swatches-menu"
|
||||
@icon="palette"
|
||||
@title={{@collapseSwatchesLabel}}
|
||||
@modalForMobile={{true}}
|
||||
class="btn-default form-kit__control-color-swatches-btn"
|
||||
>
|
||||
<:content as |args|>
|
||||
<div class="form-kit__control-color-swatches" role="group">
|
||||
{{#each @colors as |color|}}
|
||||
<button
|
||||
type="button"
|
||||
style={{colorStyle color}}
|
||||
class={{concatClass
|
||||
"form-kit__control-color-swatch"
|
||||
(if (isColorUsed @usedColors color) "is-used")
|
||||
(colorLuminanceClass color)
|
||||
}}
|
||||
title={{if
|
||||
(isColorUsed @usedColors color)
|
||||
(i18n "category.already_used")
|
||||
}}
|
||||
aria-label={{colorLabel @usedColors color}}
|
||||
data-color={{color}}
|
||||
{{on "click" (fn this.selectColor color args.close)}}
|
||||
>
|
||||
{{#if (isColorUsed @usedColors color)}}
|
||||
{{icon "check"}}
|
||||
{{/if}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</:content>
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if @colors}}
|
||||
<div class="form-kit__control-color-swatches" role="group">
|
||||
{{#each @colors as |color|}}
|
||||
<button
|
||||
type="button"
|
||||
style={{colorStyle color}}
|
||||
class={{concatClass
|
||||
"form-kit__control-color-swatch"
|
||||
(if (isColorUsed @usedColors color) "is-used")
|
||||
}}
|
||||
title={{if
|
||||
(isColorUsed @usedColors color)
|
||||
(i18n "category.already_used")
|
||||
}}
|
||||
aria-label={{colorLabel @usedColors color}}
|
||||
data-color={{color}}
|
||||
disabled={{@field.disabled}}
|
||||
{{on "click" (fn this.selectColor color)}}
|
||||
>
|
||||
{{#if (isColorUsed @usedColors color)}}
|
||||
{{icon "check"}}
|
||||
{{/if}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#unless @collapseSwatches}}
|
||||
<div class="form-kit__control-color-swatches" role="group">
|
||||
{{#each @colors as |color|}}
|
||||
<button
|
||||
type="button"
|
||||
style={{colorStyle color}}
|
||||
class={{concatClass
|
||||
"form-kit__control-color-swatch"
|
||||
(if (isColorUsed @usedColors color) "is-used")
|
||||
(colorLuminanceClass color)
|
||||
}}
|
||||
title={{if
|
||||
(isColorUsed @usedColors color)
|
||||
(i18n "category.already_used")
|
||||
}}
|
||||
aria-label={{colorLabel @usedColors color}}
|
||||
data-color={{color}}
|
||||
disabled={{@field.disabled}}
|
||||
{{on "click" (fn this.selectColor color)}}
|
||||
>
|
||||
{{#if (isColorUsed @usedColors color)}}
|
||||
{{icon "check"}}
|
||||
{{/if}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,11 +24,16 @@ const Contents = <template>
|
||||
</template>;
|
||||
|
||||
export default class FKControlConditionalContent extends Component {
|
||||
@tracked activeName = this.args.activeName;
|
||||
@tracked manuallySetName = null;
|
||||
|
||||
get activeName() {
|
||||
return this.manuallySetName ?? this.args.activeName;
|
||||
}
|
||||
|
||||
@action
|
||||
setCondition(name) {
|
||||
this.activeName = name;
|
||||
this.manuallySetName = name;
|
||||
this.args.onChange?.(name);
|
||||
}
|
||||
|
||||
<template>
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ const FKControlConditionalContentOption = <template>
|
||||
id={{uuid}}
|
||||
value={{@name}}
|
||||
checked={{eq @name @activeName}}
|
||||
disabled={{@disabled}}
|
||||
class="form-kit__control-radio"
|
||||
{{on "change" (fn @setCondition @name)}}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@ import Site from "discourse/models/site";
|
||||
import Topic from "./topic";
|
||||
|
||||
const CATEGORY_ASYNC_SEARCH_CACHE = {};
|
||||
const CATEGORY_ASYNC_HIERARCHICAL_SEARCH_CACHE = {};
|
||||
const pluginSaveProperties = new Set();
|
||||
|
||||
let _uncategorized;
|
||||
@@ -417,13 +418,27 @@ export default class Category extends RestModel {
|
||||
page: opts.page,
|
||||
};
|
||||
|
||||
const result = await ajax("/categories/hierarchical_search", { data });
|
||||
const cacheKey = JSON.stringify(data);
|
||||
CATEGORY_ASYNC_HIERARCHICAL_SEARCH_CACHE[cacheKey] ||= await ajax(
|
||||
"/categories/hierarchical_search",
|
||||
{
|
||||
method: "GET",
|
||||
data,
|
||||
}
|
||||
);
|
||||
const result = CATEGORY_ASYNC_HIERARCHICAL_SEARCH_CACHE[cacheKey];
|
||||
|
||||
return result["categories"].map((category) =>
|
||||
Site.current().updateCategory(category)
|
||||
);
|
||||
}
|
||||
|
||||
static clearAsyncHierarchicalSearchCache() {
|
||||
Object.keys(CATEGORY_ASYNC_HIERARCHICAL_SEARCH_CACHE).forEach((key) => {
|
||||
delete CATEGORY_ASYNC_HIERARCHICAL_SEARCH_CACHE[key];
|
||||
});
|
||||
}
|
||||
|
||||
static async asyncSearch(term, opts) {
|
||||
opts ||= {};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class ComboBoxHeader extends SingleSelectHeaderComponent {
|
||||
@icon="xmark"
|
||||
@action={{this.selectKit.onClearSelection}}
|
||||
@ariaLabel="clear_input"
|
||||
class="btn-clear"
|
||||
class="btn-clear btn-transparent"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ module SvgSprite
|
||||
ellipsis-vertical
|
||||
envelope
|
||||
eye
|
||||
eye-dropper
|
||||
fab-android
|
||||
fab-apple
|
||||
fab-chrome
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Reference in New Issue
Block a user