mirror of
https://github.com/discourse/discourse.git
synced 2025-08-17 18:04:11 +08:00
FEATURE: add icons and emojis to category (#31795)
This feature allow admins to personalize their communities by associating emojis or icons with their site categories. There are now 3 style types for categories: - Square (the default) - Emoji - Icon ### How it looks 🎨 Adding an icon: <img width="502" alt="Category with an icon" src="https://github.com/user-attachments/assets/8f711340-166e-4781-a7b7-7267469dbabd" /> Adding an emoji: <img width="651" alt="Category with an emoji" src="https://github.com/user-attachments/assets/588c38ce-c719-4ed5-83f9-f1e1cb52c929" /> Sidebar: <img width="248" alt="Sidebar with emojis" src="https://github.com/user-attachments/assets/cd03d591-6170-4515-998c-0cec20118568" /> Category menus: <img width="621" alt="Screenshot 2025-03-13 at 10 32 30 AM" src="https://github.com/user-attachments/assets/7d89797a-f69f-45e5-bf64-a92d4cff8753" /> Within posts/topics: <img width="382" alt="Screenshot 2025-03-13 at 10 33 41 AM" src="https://github.com/user-attachments/assets/b7b1a951-44c6-4a4f-82ad-8ee31ddd6061" /> Chat messages: <img width="392" alt="Screenshot 2025-03-13 at 10 30 20 AM" src="https://github.com/user-attachments/assets/126f8076-0ea3-4f19-8452-1041fd2af29f" /> Autocomplete: <img width="390" alt="Screenshot 2025-03-13 at 10 29 53 AM" src="https://github.com/user-attachments/assets/cad75669-225f-4b8e-a7b5-ae5aa8f1bcad" /> --------- Co-authored-by: Martin Brennan <martin@discourse.org> Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
parent
1fd553ccb0
commit
d06c60ca7c
68 changed files with 1036 additions and 361 deletions
|
@ -32,6 +32,18 @@ function addHashtag(buffer, matches, state) {
|
|||
["data-id", result.id],
|
||||
];
|
||||
|
||||
if (result.style_type) {
|
||||
token.attrs.push(["data-style-type", result.style_type]);
|
||||
}
|
||||
|
||||
if (result.style_type === "emoji" && result.emoji) {
|
||||
token.attrs.push(["data-emoji", result.emoji]);
|
||||
}
|
||||
|
||||
if (result.style_type === "icon" && result.icon) {
|
||||
token.attrs.push(["data-icon", result.icon]);
|
||||
}
|
||||
|
||||
// Most cases these will be the exact same, one standout is categories
|
||||
// which have a parent:child reference.
|
||||
if (result.slug !== result.ref) {
|
||||
|
@ -119,5 +131,8 @@ export function setup(helper) {
|
|||
"a[data-slug]",
|
||||
"a[data-ref]",
|
||||
"a[data-id]",
|
||||
"a[data-style-type]",
|
||||
"a[data-icon]",
|
||||
"a[data-emoji]",
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -165,6 +165,7 @@ export default class BreadCrumbs extends Component {
|
|||
subCategory=breadcrumb.isSubcategory
|
||||
noSubcategories=breadcrumb.noSubcategories
|
||||
autoFilterable=true
|
||||
shouldDisplayIcon=false
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
|
|
|
@ -13,6 +13,7 @@ export default class ColorPicker extends Component {
|
|||
@action
|
||||
selectColor(color) {
|
||||
this.set("value", color);
|
||||
this.onSelectColor?.(color);
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
@ -0,0 +1,337 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { fn, hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { eq } from "truth-helpers";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import categoryBadge from "discourse/helpers/category-badge";
|
||||
import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
||||
import { CATEGORY_STYLE_TYPES } from "discourse/lib/constants";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import Category from "discourse/models/category";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ColorInput from "admin/components/color-input";
|
||||
import CategoryChooser from "select-kit/components/category-chooser";
|
||||
import ColorPicker from "./color-picker";
|
||||
|
||||
export default class EditCategoryGeneral extends Component {
|
||||
@service router;
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
|
||||
uncategorizedSiteSettingLink = getURL(
|
||||
"/admin/site_settings/category/all_results?filter=allow_uncategorized_topics"
|
||||
);
|
||||
customizeTextContentLink = getURL(
|
||||
"/admin/customize/site_texts?q=uncategorized"
|
||||
);
|
||||
foregroundColors = ["FFFFFF", "000000"];
|
||||
|
||||
get styleTypes() {
|
||||
return Object.keys(CATEGORY_STYLE_TYPES).map((key) => ({
|
||||
id: key,
|
||||
name: i18n(`category.styles.options.${key}`),
|
||||
}));
|
||||
}
|
||||
|
||||
get showWarning() {
|
||||
return this.args.category.isUncategorizedCategory;
|
||||
}
|
||||
|
||||
@cached
|
||||
get backgroundColors() {
|
||||
const categories = this.site.get("categoriesList");
|
||||
return this.siteSettings.category_colors
|
||||
.split("|")
|
||||
.filter(Boolean)
|
||||
.map((i) => i.toUpperCase())
|
||||
.concat(categories.map((c) => c.color.toUpperCase()))
|
||||
.uniq();
|
||||
}
|
||||
|
||||
@cached
|
||||
get usedBackgroundColors() {
|
||||
const categories = this.site.get("categoriesList");
|
||||
const categoryId = this.args.category.id;
|
||||
const categoryColor = this.args.category.color;
|
||||
|
||||
// if editing a category, don't include its color:
|
||||
return categories
|
||||
.map((c) => {
|
||||
return categoryId &&
|
||||
categoryColor.toUpperCase() === c.color.toUpperCase()
|
||||
? null
|
||||
: c.color.toUpperCase();
|
||||
})
|
||||
.compact();
|
||||
}
|
||||
|
||||
@cached
|
||||
get parentCategories() {
|
||||
return this.site
|
||||
.get("categoriesList")
|
||||
.filter((c) => c.level + 1 < this.siteSettings.max_category_nesting);
|
||||
}
|
||||
|
||||
@action
|
||||
categoryBadgePreview(transientData) {
|
||||
const category = this.args.category;
|
||||
|
||||
const previewCategory = Category.create({
|
||||
name: transientData.name || i18n("category.untitled"),
|
||||
color: transientData.color,
|
||||
id: category.id,
|
||||
text_color: category.text_color,
|
||||
parent_category_id: parseInt(category.get("parent_category_id"), 10),
|
||||
read_restricted: category.get("read_restricted"),
|
||||
});
|
||||
|
||||
return categoryBadgeHTML(previewCategory, {
|
||||
link: false,
|
||||
previewColor: true,
|
||||
styleType: transientData.style_type,
|
||||
emoji: transientData.emoji,
|
||||
icon: transientData.icon,
|
||||
});
|
||||
}
|
||||
|
||||
// We can change the parent if there are no children
|
||||
@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
|
||||
updateColor(field, newColor) {
|
||||
field.set(newColor.replace("#", ""));
|
||||
}
|
||||
|
||||
get categoryDescription() {
|
||||
if (this.args.category.description) {
|
||||
return htmlSafe(this.args.category.description);
|
||||
}
|
||||
|
||||
return i18n("category.no_description");
|
||||
}
|
||||
|
||||
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}}
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-name-fields-details"
|
||||
@outletArgs={{hash form=@form category=@category}}
|
||||
>
|
||||
{{#unless @category.isUncategorizedCategory}}
|
||||
<@form.Field
|
||||
@name="name"
|
||||
@title={{i18n "category.name"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n "category.name_placeholder"}}
|
||||
@maxlength="50"
|
||||
class="category-name"
|
||||
/>
|
||||
</@form.Field>
|
||||
{{/unless}}
|
||||
|
||||
<@form.Field
|
||||
@name="slug"
|
||||
@title={{i18n "category.slug"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n "category.slug_placeholder"}}
|
||||
@maxlength="255"
|
||||
/>
|
||||
</@form.Field>
|
||||
</PluginOutlet>
|
||||
|
||||
{{#if this.canSelectParentCategory}}
|
||||
<@form.Field
|
||||
@name="parent_category_id"
|
||||
@title={{i18n "category.parent"}}
|
||||
@format="large"
|
||||
class="parent-category"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<CategoryChooser
|
||||
@value={{@category.parent_category_id}}
|
||||
@allowSubCategories={{true}}
|
||||
@allowRestrictedCategories={{true}}
|
||||
@onChange={{fn (mut @category.parent_category_id)}}
|
||||
@options={{hash
|
||||
allowUncategorized=false
|
||||
excludeCategoryId=@category.id
|
||||
autoInsertNoneItem=true
|
||||
none=true
|
||||
}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.subCategories}}
|
||||
<@form.Container @title={{i18n "categories.subcategories"}}>
|
||||
{{#each this.subCategories as |s|}}
|
||||
{{categoryBadge s hideParent="true"}}
|
||||
{{/each}}
|
||||
</@form.Container>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showDescription}}
|
||||
<@form.Section @title={{i18n "category.description"}}>
|
||||
{{#if @category.topic_url}}
|
||||
<@form.Container @subtitle={{this.categoryDescription}}>
|
||||
<@form.Button
|
||||
@action={{this.showCategoryTopic}}
|
||||
@icon="pencil"
|
||||
@label="category.change_in_category_topic"
|
||||
class="btn-default edit-category-description"
|
||||
/>
|
||||
</@form.Container>
|
||||
{{/if}}
|
||||
</@form.Section>
|
||||
{{/if}}
|
||||
|
||||
<@form.Section @title={{i18n "category.style"}} class="category-style">
|
||||
<@form.Field
|
||||
@name="style_type"
|
||||
@title={{i18n "category.styles.type"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
{{htmlSafe (this.categoryBadgePreview @transientData)}}
|
||||
<field.Select as |select|>
|
||||
{{#each this.styleTypes as |styleType|}}
|
||||
<select.Option @value={{styleType.id}}>
|
||||
{{styleType.name}}
|
||||
</select.Option>
|
||||
{{/each}}
|
||||
</field.Select>
|
||||
</@form.Field>
|
||||
|
||||
{{#if (eq @transientData.style_type "emoji")}}
|
||||
<@form.Field
|
||||
@name="emoji"
|
||||
@title={{i18n "category.styles.emoji"}}
|
||||
@format="small"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Emoji />
|
||||
</@form.Field>
|
||||
{{else if (eq @transientData.style_type "icon")}}
|
||||
<@form.Field
|
||||
@name="icon"
|
||||
@title={{i18n "category.styles.icon"}}
|
||||
@format="small"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Icon />
|
||||
</@form.Field>
|
||||
{{/if}}
|
||||
|
||||
{{#unless (eq @transientData.style_type "emoji")}}
|
||||
<@form.Field
|
||||
@name="color"
|
||||
@title={{i18n "category.background_color"}}
|
||||
@format="full"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<div class="category-color-editor">
|
||||
<div class="colorpicker-wrapper edit-background-color">
|
||||
<ColorInput
|
||||
@hexValue={{readonly field.value}}
|
||||
@valid={{@category.colorValid}}
|
||||
@ariaLabelledby="background-color-label"
|
||||
@onChangeColor={{fn this.updateColor field}}
|
||||
/>
|
||||
<ColorPicker
|
||||
@colors={{this.backgroundColors}}
|
||||
@usedColors={{this.usedBackgroundColors}}
|
||||
@value={{readonly field.value}}
|
||||
@ariaLabel={{i18n "category.predefined_colors"}}
|
||||
@onSelectColor={{fn this.updateColor field}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
{{/unless}}
|
||||
|
||||
<@form.Field
|
||||
@name="text_color"
|
||||
@title={{i18n "category.foreground_color"}}
|
||||
@format="full"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<div class="category-color-editor">
|
||||
<div class="colorpicker-wrapper edit-text-color">
|
||||
<ColorInput
|
||||
@hexValue={{readonly field.value}}
|
||||
@ariaLabelledby="foreground-color-label"
|
||||
@onChangeColor={{fn this.updateColor field}}
|
||||
/>
|
||||
<ColorPicker
|
||||
@colors={{this.foregroundColors}}
|
||||
@value={{readonly field.value}}
|
||||
@ariaLabel={{i18n "category.predefined_colors"}}
|
||||
@onSelectColor={{fn this.updateColor field}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</field.Custom>
|
||||
</@form.Field>
|
||||
</@form.Section>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
{{#if this.category.isUncategorizedCategory}}
|
||||
<p class="warning">
|
||||
{{d-icon "triangle-exclamation"}}
|
||||
{{html-safe
|
||||
(i18n
|
||||
"category.uncategorized_general_warning"
|
||||
settingLink=this.uncategorizedSiteSettingLink
|
||||
customizeLink=this.customizeTextContentLink
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<form>
|
||||
<CategoryNameFields @category={{this.category}} @tagName="" />
|
||||
|
||||
{{#if this.canSelectParentCategory}}
|
||||
<section class="field parent-category">
|
||||
<label>{{i18n "category.parent"}}</label>
|
||||
<CategoryChooser
|
||||
@value={{this.category.parent_category_id}}
|
||||
@allowSubCategories={{true}}
|
||||
@allowRestrictedCategories={{true}}
|
||||
@onChange={{fn (mut this.category.parent_category_id)}}
|
||||
@options={{hash
|
||||
allowUncategorized=false
|
||||
excludeCategoryId=this.category.id
|
||||
autoInsertNoneItem=true
|
||||
none=true
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.subCategories}}
|
||||
<section class="field subcategories">
|
||||
<label>{{i18n "categories.subcategories"}}</label>
|
||||
{{#each this.subCategories as |s|}}
|
||||
{{category-badge s hideParent="true"}}
|
||||
{{/each}}
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showDescription}}
|
||||
<section class="field description">
|
||||
<label>{{i18n "category.description"}}</label>
|
||||
{{#if this.category.description}}
|
||||
{{html-safe this.category.description}}
|
||||
{{else}}
|
||||
{{i18n "category.no_description"}}
|
||||
{{/if}}
|
||||
{{#if this.category.topic_url}}
|
||||
<br />
|
||||
<DButton
|
||||
@action={{this.showCategoryTopic}}
|
||||
@icon="pencil"
|
||||
@label="category.change_in_category_topic"
|
||||
class="btn-default edit-category-description"
|
||||
/>
|
||||
{{/if}}
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class="field category-colors">
|
||||
<label>{{i18n "category.badge_colors"}}</label>
|
||||
<div class="category-color-editor">
|
||||
{{html-safe this.categoryBadgePreview}}
|
||||
|
||||
<section class="field">
|
||||
<span id="background-color-label" class="color-title">{{i18n
|
||||
"category.background_color"
|
||||
}}:</span>
|
||||
<div class="colorpicker-wrapper">
|
||||
<ColorInput
|
||||
@hexValue={{this.category.color}}
|
||||
@valid={{this.category.colorValid}}
|
||||
@ariaLabelledby="background-color-label"
|
||||
/>
|
||||
<ColorPicker
|
||||
@colors={{this.backgroundColors}}
|
||||
@usedColors={{this.usedBackgroundColors}}
|
||||
@value={{this.category.color}}
|
||||
@ariaLabel={{i18n "category.predefined_colors"}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<span id="foreground-color-label" class="color-title">{{i18n
|
||||
"category.foreground_color"
|
||||
}}:</span>
|
||||
<div class="colorpicker-wrapper edit-text-color">
|
||||
<ColorInput
|
||||
@hexValue={{this.category.text_color}}
|
||||
@ariaLabelledby="foreground-color-label"
|
||||
/>
|
||||
<ColorPicker
|
||||
@colors={{this.foregroundColors}}
|
||||
@value={{this.category.text_color}}
|
||||
@id="edit-text-color"
|
||||
@ariaLabel={{i18n "category.predefined_colors"}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
|
@ -1,124 +0,0 @@
|
|||
import { action } from "@ember/object";
|
||||
import { not } from "@ember/object/computed";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
|
||||
import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import discourseLater from "discourse/lib/later";
|
||||
import Category from "discourse/models/category";
|
||||
|
||||
export default class EditCategoryGeneral extends buildCategoryPanel("general") {
|
||||
@not("category.isUncategorizedCategory") canSelectParentCategory;
|
||||
|
||||
uncategorizedSiteSettingLink = getURL(
|
||||
"/admin/site_settings/category/all_results?filter=allow_uncategorized_topics"
|
||||
);
|
||||
customizeTextContentLink = getURL(
|
||||
"/admin/customize/site_texts?q=uncategorized"
|
||||
);
|
||||
|
||||
foregroundColors = ["FFFFFF", "000000"];
|
||||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
|
||||
this._focusCategoryName();
|
||||
}
|
||||
|
||||
willDestroyElement() {
|
||||
super.willDestroyElement(...arguments);
|
||||
|
||||
this._laterFocus && cancel(this._laterFocus);
|
||||
}
|
||||
|
||||
// background colors are available as a pipe-separated string
|
||||
@discourseComputed
|
||||
backgroundColors() {
|
||||
const categories = this.site.get("categoriesList");
|
||||
return this.siteSettings.category_colors
|
||||
.split("|")
|
||||
.map(function (i) {
|
||||
return i.toUpperCase();
|
||||
})
|
||||
.concat(
|
||||
categories.map(function (c) {
|
||||
return c.color.toUpperCase();
|
||||
})
|
||||
)
|
||||
.uniq();
|
||||
}
|
||||
|
||||
@discourseComputed("category.id", "category.color")
|
||||
usedBackgroundColors(categoryId, categoryColor) {
|
||||
const categories = this.site.get("categoriesList");
|
||||
|
||||
// If editing a category, don't include its color:
|
||||
return categories
|
||||
.map(function (c) {
|
||||
return categoryId &&
|
||||
categoryColor.toUpperCase() === c.color.toUpperCase()
|
||||
? null
|
||||
: c.color.toUpperCase();
|
||||
}, this)
|
||||
.compact();
|
||||
}
|
||||
|
||||
@discourseComputed
|
||||
parentCategories() {
|
||||
return this.site
|
||||
.get("categoriesList")
|
||||
.filter((c) => c.level + 1 < this.siteSettings.max_category_nesting);
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"category.parent_category_id",
|
||||
"category.name",
|
||||
"category.color",
|
||||
"category.text_color"
|
||||
)
|
||||
categoryBadgePreview(parentCategoryId, name, color, textColor) {
|
||||
const category = this.category;
|
||||
const c = Category.create({
|
||||
name,
|
||||
color,
|
||||
id: category.id,
|
||||
text_color: textColor,
|
||||
parent_category_id: parseInt(parentCategoryId, 10),
|
||||
read_restricted: category.get("read_restricted"),
|
||||
});
|
||||
return categoryBadgeHTML(c, { link: false, previewColor: true });
|
||||
}
|
||||
|
||||
// We can change the parent if there are no children
|
||||
@discourseComputed("category.id")
|
||||
subCategories(categoryId) {
|
||||
if (isEmpty(categoryId)) {
|
||||
return null;
|
||||
}
|
||||
return Category.list().filterBy("parent_category_id", categoryId);
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"category.isUncategorizedCategory",
|
||||
"category.id",
|
||||
"category.topic_url"
|
||||
)
|
||||
showDescription(isUncategorizedCategory, categoryId, topicUrl) {
|
||||
return !isUncategorizedCategory && categoryId && topicUrl;
|
||||
}
|
||||
|
||||
@action
|
||||
showCategoryTopic() {
|
||||
window.open(this.get("category.topic_url"), "_blank").focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
_focusCategoryName() {
|
||||
this._laterFocus = discourseLater(() => {
|
||||
const categoryName = this.element.querySelector(".category-name");
|
||||
categoryName && categoryName.focus();
|
||||
}, 25);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ export default class EditCategoryPanel extends Component {}
|
|||
export function buildCategoryPanel(tab) {
|
||||
@classNameBindings(
|
||||
":edit-category-tab",
|
||||
"activeTab::hide",
|
||||
"activeTab:active",
|
||||
`:edit-category-tab-${tab}`
|
||||
)
|
||||
class BuiltCategoryPanel extends EditCategoryPanel {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { eq } from "truth-helpers";
|
|||
import { isHex } from "discourse/components/sidebar/section-link";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
|
||||
export default class SidebarSectionLinkPrefix extends Component {
|
||||
get prefixValue() {
|
||||
|
@ -13,7 +14,9 @@ export default class SidebarSectionLinkPrefix extends Component {
|
|||
}
|
||||
|
||||
switch (this.args.prefixType) {
|
||||
case "span":
|
||||
case "emoji":
|
||||
return `:${this.args.prefixValue}:`;
|
||||
case "square":
|
||||
let hexValues = this.args.prefixValue;
|
||||
|
||||
hexValues = hexValues.reduce((acc, color) => {
|
||||
|
@ -54,14 +57,16 @@ export default class SidebarSectionLinkPrefix extends Component {
|
|||
</span>
|
||||
{{else if (eq @prefixType "icon")}}
|
||||
{{icon this.prefixValue class="prefix-icon"}}
|
||||
{{else if (eq @prefixType "span")}}
|
||||
{{else if (eq @prefixType "emoji")}}
|
||||
{{replaceEmoji this.prefixValue class="prefix-emoji"}}
|
||||
{{else if (eq @prefixType "square")}}
|
||||
<span
|
||||
style={{htmlSafe
|
||||
(concat
|
||||
"background: linear-gradient(90deg, " this.prefixValue ")"
|
||||
)
|
||||
}}
|
||||
class="prefix-span"
|
||||
class="prefix-square"
|
||||
></span>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { action, getProperties } from "@ember/object";
|
||||
import { and } from "@ember/object/computed";
|
||||
import { service } from "@ember/service";
|
||||
import { underscore } from "@ember/string";
|
||||
|
@ -11,11 +12,25 @@ import Category from "discourse/models/category";
|
|||
import PermissionType from "discourse/models/permission-type";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const FIELD_LIST = [
|
||||
"name",
|
||||
"slug",
|
||||
"parent_category_id",
|
||||
"description",
|
||||
"color",
|
||||
"text_color",
|
||||
"style_type",
|
||||
"emoji",
|
||||
"icon",
|
||||
];
|
||||
|
||||
export default class EditCategoryTabsController extends Controller {
|
||||
@service dialog;
|
||||
@service site;
|
||||
@service router;
|
||||
|
||||
@tracked breadcrumbCategories = this.site.get("categoriesList");
|
||||
|
||||
selectedTab = "general";
|
||||
saving = false;
|
||||
deleting = false;
|
||||
|
@ -28,18 +43,31 @@ export default class EditCategoryTabsController extends Controller {
|
|||
|
||||
@and("showTooltip", "model.cannot_delete_reason") showDeleteReason;
|
||||
|
||||
@discourseComputed("saving", "model.name", "model.color", "deleting")
|
||||
disabled(saving, name, color, deleting) {
|
||||
if (saving || deleting) {
|
||||
get formData() {
|
||||
const data = getProperties(this.model, ...FIELD_LIST);
|
||||
|
||||
if (!this.model.styleType) {
|
||||
data.style_type = "square";
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@action
|
||||
canSaveForm(transientData) {
|
||||
if (!transientData.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!transientData.color) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.saving || this.deleting) {
|
||||
return true;
|
||||
}
|
||||
if (!name) {
|
||||
return true;
|
||||
}
|
||||
if (!color) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@discourseComputed("saving", "deleting")
|
||||
|
@ -81,11 +109,17 @@ export default class EditCategoryTabsController extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
saveCategory() {
|
||||
isLeavingForm(transition) {
|
||||
return !transition.targetName.startsWith("editCategory.tabs");
|
||||
}
|
||||
|
||||
@action
|
||||
saveCategory(transientData) {
|
||||
if (this.validators.some((validator) => validator())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.setProperties(transientData);
|
||||
this.set("saving", true);
|
||||
|
||||
this.model
|
||||
|
@ -105,6 +139,10 @@ export default class EditCategoryTabsController extends Controller {
|
|||
Category.slugFor(this.model)
|
||||
);
|
||||
}
|
||||
// force a reload of the category list to track changes to style type
|
||||
this.breadcrumbCategories = this.site.categoriesList.map((c) =>
|
||||
c.id === this.model.id ? this.model : c
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
popupAjaxError(error);
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import EmojiPicker from "discourse/components/emoji-picker";
|
||||
|
||||
export default class FKControlEmoji extends Component {
|
||||
static controlType = "emoji";
|
||||
|
||||
@action
|
||||
updateField(value) {
|
||||
this.args.field.set(value);
|
||||
}
|
||||
|
||||
<template>
|
||||
<EmojiPicker
|
||||
@emoji={{@field.value}}
|
||||
@context={{@context}}
|
||||
@didSelectEmoji={{this.updateField}}
|
||||
@modalForMobile={{false}}
|
||||
@btnClass="btn-emoji"
|
||||
/>
|
||||
</template>
|
||||
}
|
|
@ -8,6 +8,7 @@ import FKControlCheckbox from "discourse/form-kit/components/fk/control/checkbox
|
|||
import FKControlCode from "discourse/form-kit/components/fk/control/code";
|
||||
import FKControlComposer from "discourse/form-kit/components/fk/control/composer";
|
||||
import FKControlCustom from "discourse/form-kit/components/fk/control/custom";
|
||||
import FKControlEmoji from "discourse/form-kit/components/fk/control/emoji";
|
||||
import FKControlIcon from "discourse/form-kit/components/fk/control/icon";
|
||||
import FKControlImage from "discourse/form-kit/components/fk/control/image";
|
||||
import FKControlInput from "discourse/form-kit/components/fk/control/input";
|
||||
|
@ -103,6 +104,7 @@ export default class FKField extends Component {
|
|||
Password=(this.componentFor FKControlPassword field)
|
||||
Composer=(this.componentFor FKControlComposer field)
|
||||
Icon=(this.componentFor FKControlIcon field)
|
||||
Emoji=(this.componentFor FKControlEmoji field)
|
||||
Toggle=(this.componentFor FKControlToggle field)
|
||||
Menu=(this.componentFor FKControlMenu field)
|
||||
Select=(this.componentFor FKControlSelect field)
|
||||
|
|
|
@ -59,11 +59,20 @@ class FKForm extends Component {
|
|||
|
||||
@action
|
||||
async checkIsDirty(transition) {
|
||||
if (
|
||||
let triggerConfirm = false;
|
||||
|
||||
const shouldCheck =
|
||||
this.formData.isDirty &&
|
||||
!transition.isAborted &&
|
||||
!transition.queryParamsOnly
|
||||
) {
|
||||
!transition.queryParamsOnly;
|
||||
|
||||
if (this.args.onDirtyCheck) {
|
||||
triggerConfirm = shouldCheck && this.args.onDirtyCheck(transition);
|
||||
} else {
|
||||
triggerConfirm = shouldCheck;
|
||||
}
|
||||
|
||||
if (triggerConfirm) {
|
||||
transition.abort();
|
||||
|
||||
this.dialog.yesNoConfirm({
|
||||
|
@ -316,6 +325,7 @@ const Form = <template>
|
|||
@validateOn={{@validateOn}}
|
||||
@onRegisterApi={{@onRegisterApi}}
|
||||
@onReset={{@onReset}}
|
||||
@onDirtyCheck={{@onDirtyCheck}}
|
||||
...attributes
|
||||
as |components draftData|
|
||||
>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { get } from "@ember/object";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import categoryVariables from "discourse/helpers/category-variables";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { helperContext, registerRawHelper } from "discourse/lib/helpers";
|
||||
import { iconHTML } from "discourse/lib/icon-library";
|
||||
|
@ -48,12 +49,24 @@ export function categoryBadgeHTML(category, opts) {
|
|||
return "";
|
||||
}
|
||||
|
||||
if (!opts.styleType) {
|
||||
opts.styleType = category.styleType;
|
||||
|
||||
if (opts.styleType === "icon") {
|
||||
opts.icon = category.icon;
|
||||
} else if (opts.styleType === "emoji") {
|
||||
opts.emoji = category.emoji;
|
||||
}
|
||||
}
|
||||
|
||||
const depth = (opts.depth || 1) + 1;
|
||||
if (opts.ancestors) {
|
||||
const { ancestors, ...otherOpts } = opts;
|
||||
return [category, ...ancestors]
|
||||
.reverse()
|
||||
.map((c) => categoryBadgeHTML(c, otherOpts))
|
||||
.map((c) => {
|
||||
return categoryBadgeHTML(c, { ...otherOpts, styleType: null });
|
||||
})
|
||||
.join("");
|
||||
} else if (opts.recursive && depth <= siteSettings.max_category_nesting) {
|
||||
const parentCategory = Category.findById(category.parent_category_id);
|
||||
|
@ -151,6 +164,10 @@ export function defaultCategoryLinkRenderer(category, opts) {
|
|||
dataAttributes += ` data-parent-category-id="${parentCat.id}"`;
|
||||
}
|
||||
|
||||
if (opts.styleType) {
|
||||
classNames += ` --style-${opts.styleType}`;
|
||||
}
|
||||
|
||||
html += `<span
|
||||
${dataAttributes}
|
||||
data-drop-close="true"
|
||||
|
@ -163,6 +180,14 @@ export function defaultCategoryLinkRenderer(category, opts) {
|
|||
${descriptionText ? 'title="' + descriptionText + '" ' : ""}
|
||||
>`;
|
||||
|
||||
if (opts.styleType === "icon" && opts.icon) {
|
||||
html += iconHTML(opts.icon);
|
||||
}
|
||||
|
||||
if (opts.styleType === "emoji" && opts.emoji) {
|
||||
html += replaceEmoji(`:${opts.emoji}:`);
|
||||
}
|
||||
|
||||
// not ideal as we have to call it manually and we pass a fake category object
|
||||
// but there's not way around it for now
|
||||
let categoryName = applyValueTransformer(
|
||||
|
|
|
@ -22,6 +22,8 @@ export const SIDEBAR_SECTION = {
|
|||
max_title_length: 30,
|
||||
};
|
||||
|
||||
export const CATEGORY_STYLE_TYPES = { square: 0, icon: 1, emoji: 2 };
|
||||
|
||||
export const AUTO_GROUPS = {
|
||||
everyone: {
|
||||
id: 0,
|
||||
|
|
|
@ -126,13 +126,27 @@ function _searchRequest(term, contextualHashtagConfiguration, resultFunc) {
|
|||
// Convert :emoji: in the result text to HTML safely.
|
||||
result.text = htmlSafe(emojiUnescape(escapeExpression(result.text)));
|
||||
|
||||
const hashtagType = getHashtagTypeClassesNew()[result.type];
|
||||
result.icon = hashtagType.generateIconHTML({
|
||||
let opts = {
|
||||
preloaded: true,
|
||||
colors: result.colors,
|
||||
icon: result.icon,
|
||||
id: result.id,
|
||||
});
|
||||
};
|
||||
|
||||
if (result.style_type) {
|
||||
opts.style_type = result.style_type;
|
||||
}
|
||||
|
||||
if (result.icon) {
|
||||
opts.icon = result.icon;
|
||||
}
|
||||
|
||||
if (result.emoji) {
|
||||
opts.emoji = result.emoji;
|
||||
}
|
||||
|
||||
const hashtagType = getHashtagTypeClassesNew()[result.type];
|
||||
result.icon = hashtagType.generateIconHTML(opts);
|
||||
});
|
||||
resultFunc(response.results || CANCELLED_STATUS);
|
||||
})
|
||||
|
|
|
@ -80,6 +80,15 @@ export function generatePlaceholderHashtagHTML(type, spanEl, data) {
|
|||
link.dataset.type = type;
|
||||
link.dataset.id = data.id;
|
||||
link.dataset.slug = data.slug;
|
||||
link.dataset.style_type = data.style_type;
|
||||
|
||||
if (data.style_type === "icon") {
|
||||
link.dataset.icon = data.icon;
|
||||
}
|
||||
if (data.style_type === "emoji") {
|
||||
link.dataset.emoji = data.emoji;
|
||||
}
|
||||
|
||||
const hashtagTypeClass = new getHashtagTypeClasses()[type];
|
||||
link.innerHTML = `${hashtagTypeClass.generateIconHTML(
|
||||
data
|
||||
|
@ -96,13 +105,21 @@ export function decorateHashtags(element, site) {
|
|||
const hashtagType = hashtagEl.dataset.type;
|
||||
const hashtagTypeClass = getHashtagTypeClasses()[hashtagType];
|
||||
if (iconPlaceholderEl && hashtagTypeClass) {
|
||||
const hashtagIconHTML = hashtagTypeClass
|
||||
.generateIconHTML({
|
||||
icon: site.hashtag_icons[hashtagType],
|
||||
id: hashtagEl.dataset.id,
|
||||
slug: hashtagEl.dataset.slug,
|
||||
})
|
||||
.trim();
|
||||
let opts = {
|
||||
icon: site.hashtag_icons[hashtagType],
|
||||
id: hashtagEl.dataset.id,
|
||||
slug: hashtagEl.dataset.slug,
|
||||
style_type: hashtagEl.dataset.styleType,
|
||||
};
|
||||
|
||||
if (hashtagEl.dataset.styleType === "icon") {
|
||||
opts.icon = hashtagEl.dataset.icon;
|
||||
}
|
||||
if (hashtagEl.dataset.styleType === "emoji") {
|
||||
opts.emoji = hashtagEl.dataset.emoji;
|
||||
}
|
||||
|
||||
const hashtagIconHTML = hashtagTypeClass.generateIconHTML(opts).trim();
|
||||
iconPlaceholderEl.replaceWith(domFromString(hashtagIconHTML)[0]);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { service } from "@ember/service";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import { iconHTML } from "discourse/lib/icon-library";
|
||||
import HashtagTypeBase from "./base";
|
||||
|
||||
export default class CategoryHashtagType extends HashtagTypeBase {
|
||||
|
@ -17,7 +19,7 @@ export default class CategoryHashtagType extends HashtagTypeBase {
|
|||
// Set a default color for category hashtags. This is added here instead
|
||||
// of `hashtag.scss` because of the CSS precedence rules (<link> has a
|
||||
// higher precedence than <style>)
|
||||
".hashtag-category-badge { background-color: var(--primary-medium); }",
|
||||
".hashtag-category-square { background-color: var(--primary-medium); }",
|
||||
...super.generatePreloadedCssClasses(),
|
||||
];
|
||||
}
|
||||
|
@ -33,7 +35,10 @@ export default class CategoryHashtagType extends HashtagTypeBase {
|
|||
}
|
||||
} else {
|
||||
color = categoryOrHashtag.color;
|
||||
if (categoryOrHashtag.parentCategory) {
|
||||
if (
|
||||
categoryOrHashtag.parentCategory &&
|
||||
categoryOrHashtag.styleType === "square"
|
||||
) {
|
||||
parentColor = categoryOrHashtag.parentCategory.color;
|
||||
}
|
||||
}
|
||||
|
@ -41,8 +46,12 @@ export default class CategoryHashtagType extends HashtagTypeBase {
|
|||
let style;
|
||||
if (parentColor) {
|
||||
style = `background: linear-gradient(-90deg, #${color} 50%, #${parentColor} 50%);`;
|
||||
} else {
|
||||
} else if (categoryOrHashtag.styleType === "icon") {
|
||||
style = `color: #${color};`;
|
||||
} else if (categoryOrHashtag.styleType === "square") {
|
||||
style = `background-color: #${color};`;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [`.hashtag-color--category-${categoryOrHashtag.id} { ${style} }`];
|
||||
|
@ -50,9 +59,17 @@ export default class CategoryHashtagType extends HashtagTypeBase {
|
|||
|
||||
generateIconHTML(hashtag) {
|
||||
hashtag.preloaded ? this.onLoad(hashtag) : this.load(hashtag.id);
|
||||
let style = "";
|
||||
|
||||
if (hashtag.style_type === "icon" && hashtag.icon) {
|
||||
style = iconHTML(hashtag.icon);
|
||||
}
|
||||
if (hashtag.style_type === "emoji" && hashtag.emoji) {
|
||||
style = replaceEmoji(`:${hashtag.emoji}:`);
|
||||
}
|
||||
|
||||
const colorCssClass = `hashtag-color--${this.type}-${hashtag.id}`;
|
||||
return `<span class="hashtag-category-badge ${colorCssClass}"></span>`;
|
||||
return `<span class="hashtag-category-${hashtag.style_type} ${colorCssClass}">${style}</span>`;
|
||||
}
|
||||
|
||||
isLoaded(id) {
|
||||
|
|
|
@ -186,7 +186,14 @@ export default class CategorySectionLink {
|
|||
}
|
||||
|
||||
get prefixType() {
|
||||
return customCategoryPrefixes[this.category.id]?.prefixType || "span";
|
||||
const customPrefixType =
|
||||
customCategoryPrefixes[this.category.id]?.prefixType;
|
||||
|
||||
if (customPrefixType) {
|
||||
return customPrefixType;
|
||||
}
|
||||
|
||||
return this.category.styleType;
|
||||
}
|
||||
|
||||
get prefixValue() {
|
||||
|
@ -197,6 +204,16 @@ export default class CategorySectionLink {
|
|||
return customPrefixValue;
|
||||
}
|
||||
|
||||
const styleType = this.category.styleType;
|
||||
|
||||
if (styleType === "icon") {
|
||||
return this.category.icon;
|
||||
}
|
||||
|
||||
if (styleType === "emoji") {
|
||||
return this.category.emoji;
|
||||
}
|
||||
|
||||
if (this.category.parentCategory?.color) {
|
||||
return [this.category.parentCategory?.color, this.category.color];
|
||||
} else {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { warn } from "@ember/debug";
|
||||
import { computed, get } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
|
@ -451,6 +452,10 @@ export default class Category extends RestModel {
|
|||
|
||||
@service currentUser;
|
||||
|
||||
@tracked color;
|
||||
@tracked styleType = this.style_type;
|
||||
@tracked emoji;
|
||||
@tracked icon;
|
||||
permissions = null;
|
||||
|
||||
init() {
|
||||
|
@ -710,6 +715,8 @@ export default class Category extends RestModel {
|
|||
const id = this.id;
|
||||
const url = id ? `/categories/${id}` : "/categories";
|
||||
|
||||
this.styleType = this.style_type;
|
||||
|
||||
return ajax(url, {
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({
|
||||
|
@ -761,6 +768,9 @@ export default class Category extends RestModel {
|
|||
moderating_group_ids: this.moderating_group_ids,
|
||||
read_only_banner: this.read_only_banner,
|
||||
default_list_filter: this.default_list_filter,
|
||||
style_type: this.style_type,
|
||||
emoji: this.emoji,
|
||||
icon: this.icon,
|
||||
}),
|
||||
type: id ? "PUT" : "POST",
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<h2>{{this.title}}</h2>
|
||||
{{#if this.model.id}}
|
||||
<BreadCrumbs
|
||||
@categories={{this.site.categoriesList}}
|
||||
@categories={{this.breadcrumbCategories}}
|
||||
@category={{this.model}}
|
||||
@noSubcategories={{this.model.noSubcategories}}
|
||||
@editingCategory={{true}}
|
||||
|
@ -65,52 +65,59 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="edit-category-content">
|
||||
<h3>{{this.selectedTabTitle}}</h3>
|
||||
<Form
|
||||
@data={{this.formData}}
|
||||
@onDirtyCheck={{this.isLeavingForm}}
|
||||
as |form transientData|
|
||||
>
|
||||
<form.Section
|
||||
@title={{this.selectedTabTitle}}
|
||||
class="edit-category-content"
|
||||
>
|
||||
{{#each this.panels as |tab|}}
|
||||
{{component
|
||||
tab
|
||||
selectedTab=this.selectedTab
|
||||
category=this.model
|
||||
action=this.registerValidator
|
||||
transientData=transientData
|
||||
form=form
|
||||
}}
|
||||
{{/each}}
|
||||
</form.Section>
|
||||
|
||||
{{#each this.panels as |tab|}}
|
||||
{{component
|
||||
tab
|
||||
selectedTab=this.selectedTab
|
||||
category=this.model
|
||||
registerValidator=(action "registerValidator")
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if this.showDeleteReason}}
|
||||
<form.Alert @type="warning" class="edit-category-delete-warning">
|
||||
{{html-safe this.model.cannot_delete_reason}}
|
||||
</form.Alert>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showDeleteReason}}
|
||||
<div class="edit-category-delete-warning">
|
||||
<p class="warning">{{html-safe this.model.cannot_delete_reason}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="edit-category-footer">
|
||||
<DButton
|
||||
@disabled={{this.disabled}}
|
||||
@action={{action "saveCategory"}}
|
||||
@label={{this.saveLabel}}
|
||||
id="save-category"
|
||||
class="btn-primary"
|
||||
/>
|
||||
|
||||
{{#if this.model.can_delete}}
|
||||
<DButton
|
||||
@disabled={{this.deleteDisabled}}
|
||||
@action={{action "deleteCategory"}}
|
||||
@icon="trash-can"
|
||||
@label="category.delete"
|
||||
class="btn-danger"
|
||||
<form.Actions class="edit-category-footer">
|
||||
<form.Button
|
||||
@disabled={{not (this.canSaveForm transientData)}}
|
||||
@action={{fn this.saveCategory transientData}}
|
||||
@label={{this.saveLabel}}
|
||||
id="save-category"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{else if this.model.id}}
|
||||
<div class="disable-info">
|
||||
<DButton
|
||||
|
||||
{{#if this.model.can_delete}}
|
||||
<form.Button
|
||||
@disabled={{this.deleteDisabled}}
|
||||
@action={{action "toggleDeleteTooltip"}}
|
||||
@action={{this.deleteCategory}}
|
||||
@icon="trash-can"
|
||||
@label="category.delete"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{else if this.model.id}}
|
||||
<form.Button
|
||||
@disabled={{this.deleteDisabled}}
|
||||
@action={{this.toggleDeleteTooltip}}
|
||||
@icon="circle-question"
|
||||
@label="category.delete"
|
||||
class="btn-default"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</form.Actions>
|
||||
</Form>
|
||||
</div>
|
|
@ -22,20 +22,20 @@ acceptance("Category Edit", function (needs) {
|
|||
);
|
||||
|
||||
assert.dom(".category-breadcrumb .badge-category").hasText("bug");
|
||||
assert.dom(".category-color-editor .badge-category").hasText("bug");
|
||||
assert.dom(".badge-category__wrapper .badge-category").hasText("bug");
|
||||
await fillIn("input.category-name", "testing");
|
||||
assert.dom(".category-color-editor .badge-category").hasText("testing");
|
||||
assert.dom(".category-style .badge-category__name").hasText("testing");
|
||||
|
||||
await fillIn(".edit-text-color input", "ff0000");
|
||||
|
||||
await click(".edit-category-topic-template");
|
||||
await click(".edit-category-topic-template a");
|
||||
await fillIn(".d-editor-input", "this is the new topic template");
|
||||
|
||||
await click("#save-category");
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
"/c/bug/edit/general",
|
||||
"stays on the edit screen"
|
||||
"/c/bug/edit/topic-template",
|
||||
"stays on the topic template screen"
|
||||
);
|
||||
|
||||
await visit("/c/bug/edit/settings");
|
||||
|
@ -47,7 +47,7 @@ acceptance("Category Edit", function (needs) {
|
|||
assert.strictEqual(
|
||||
currentURL(),
|
||||
"/c/bug/edit/settings",
|
||||
"stays on the edit screen"
|
||||
"stays on the settings screen"
|
||||
);
|
||||
|
||||
sinon.stub(DiscourseURL, "routeTo");
|
||||
|
|
|
@ -47,8 +47,8 @@ acceptance("Category New", function (needs) {
|
|||
|
||||
await click(".edit-category-nav .edit-category-topic-template a");
|
||||
assert
|
||||
.dom(".edit-category-tab-topic-template")
|
||||
.isVisible("it can switch to topic template tab");
|
||||
.dom(".edit-category-tab-topic-template.active")
|
||||
.exists("it can switch to the topic template tab");
|
||||
|
||||
await click(".edit-category-nav .edit-category-tags a");
|
||||
await click("button.add-required-tag-group");
|
||||
|
@ -106,7 +106,7 @@ acceptance("New category preview", function (needs) {
|
|||
await visit("/new-category");
|
||||
|
||||
let previewBadgeColor = document
|
||||
.querySelector(".category-color-editor .badge-category")
|
||||
.querySelector(".category-style .badge-category")
|
||||
.style.getPropertyValue("--category-badge-color")
|
||||
.trim();
|
||||
|
||||
|
@ -115,7 +115,7 @@ acceptance("New category preview", function (needs) {
|
|||
await fillIn(".hex-input", "FF00FF");
|
||||
|
||||
previewBadgeColor = document
|
||||
.querySelector(".category-color-editor .badge-category")
|
||||
.querySelector(".category-style .badge-category")
|
||||
.style.getPropertyValue("--category-badge-color")
|
||||
.trim();
|
||||
|
||||
|
|
|
@ -7,12 +7,25 @@ acceptance("Hashtag CSS Generator", function (needs) {
|
|||
|
||||
needs.site({
|
||||
categories: [
|
||||
{ id: 1, color: "ff0000", text_color: "ffffff", name: "category1" },
|
||||
{ id: 2, color: "333", text_color: "ffffff", name: "category2" },
|
||||
{
|
||||
id: 1,
|
||||
color: "ff0000",
|
||||
text_color: "ffffff",
|
||||
style_type: "square",
|
||||
name: "category1",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
color: "333",
|
||||
text_color: "ffffff",
|
||||
style_type: "square",
|
||||
name: "category2",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
color: "2B81AF",
|
||||
text_color: "ffffff",
|
||||
style_type: "square",
|
||||
parent_category_id: 1,
|
||||
name: "category3",
|
||||
},
|
||||
|
@ -25,7 +38,7 @@ acceptance("Hashtag CSS Generator", function (needs) {
|
|||
assert
|
||||
.dom(cssTag)
|
||||
.hasHtml(
|
||||
".hashtag-category-badge { background-color: var(--primary-medium); }\n" +
|
||||
".hashtag-category-square { background-color: var(--primary-medium); }\n" +
|
||||
".hashtag-color--category-1 { background-color: #ff0000; }\n" +
|
||||
".hashtag-color--category-2 { background-color: #333; }\n" +
|
||||
".hashtag-color--category-4 { background: linear-gradient(-90deg, #2B81AF 50%, #ff0000 50%); }"
|
||||
|
|
|
@ -426,7 +426,7 @@ acceptance("Sidebar - Logged on user - Categories Section", function (needs) {
|
|||
|
||||
assert
|
||||
.dom(
|
||||
`.sidebar-section-link-wrapper[data-category-id="${category1.id}"] .sidebar-section-link-prefix .prefix-span[style="background: linear-gradient(90deg, #${category1.color} 50%, #${category1.color} 50%)"]`
|
||||
`.sidebar-section-link-wrapper[data-category-id="${category1.id}"] .sidebar-section-link-prefix .prefix-square[style="background: linear-gradient(90deg, #${category1.color} 50%, #${category1.color} 50%)"]`
|
||||
)
|
||||
.exists(
|
||||
"category1 section link is rendered with solid prefix icon color"
|
||||
|
@ -490,7 +490,7 @@ acceptance("Sidebar - Logged on user - Categories Section", function (needs) {
|
|||
|
||||
assert
|
||||
.dom(
|
||||
`.sidebar-section-link-wrapper[data-category-id="${category4.id}"] .sidebar-section-link-prefix .prefix-span[style="background: linear-gradient(90deg, #${category4.parentCategory.color} 50%, #${category4.color} 50%)"]`
|
||||
`.sidebar-section-link-wrapper[data-category-id="${category4.id}"] .sidebar-section-link-prefix .prefix-square[style="background: linear-gradient(90deg, #${category4.parentCategory.color} 50%, #${category4.color} 50%)"]`
|
||||
)
|
||||
.exists("sub category section link is rendered with double prefix color");
|
||||
});
|
||||
|
@ -630,7 +630,7 @@ acceptance("Sidebar - Logged on user - Categories Section", function (needs) {
|
|||
|
||||
assert
|
||||
.dom(
|
||||
`.sidebar-section-link-wrapper[data-category-id="${category1.id}"] .sidebar-section-link-prefix .prefix-span[style="background: linear-gradient(90deg, #888 50%, #888 50%)"]`
|
||||
`.sidebar-section-link-wrapper[data-category-id="${category1.id}"] .sidebar-section-link-prefix .prefix-square[style="background: linear-gradient(90deg, #888 50%, #888 50%)"]`
|
||||
)
|
||||
.exists(
|
||||
"category1 section link is rendered with the right solid prefix icon color"
|
||||
|
|
|
@ -5,6 +5,7 @@ export default {
|
|||
name: "bug",
|
||||
color: "e9dd00",
|
||||
text_color: "000000",
|
||||
style_type: "square",
|
||||
slug: "bug",
|
||||
topic_count: 2030,
|
||||
post_count: 13418,
|
||||
|
@ -45,6 +46,7 @@ export default {
|
|||
name: "testing",
|
||||
color: "0088CC",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "testing",
|
||||
can_edit: true,
|
||||
},
|
||||
|
@ -55,6 +57,7 @@ export default {
|
|||
name: "restricted-group",
|
||||
color: "e9dd00",
|
||||
text_color: "000000",
|
||||
style_type: "square",
|
||||
slug: "restricted-group",
|
||||
read_restricted: true,
|
||||
permission: null,
|
||||
|
|
|
@ -1433,6 +1433,7 @@ export default {
|
|||
name: "bug",
|
||||
color: "e9dd00",
|
||||
text_color: "000000",
|
||||
style_type: "square",
|
||||
slug: "bug",
|
||||
topic_count: 660,
|
||||
description:
|
||||
|
@ -1535,6 +1536,7 @@ export default {
|
|||
name: "feature",
|
||||
color: "0E76BD",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "feature",
|
||||
topic_count: 727,
|
||||
description:
|
||||
|
@ -1560,6 +1562,7 @@ export default {
|
|||
name: "spec",
|
||||
color: "33B0B0",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "spec",
|
||||
topic_count: 20,
|
||||
post_count: 278,
|
||||
|
@ -1657,6 +1660,7 @@ export default {
|
|||
name: "support",
|
||||
color: "b99",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "support",
|
||||
topic_count: 782,
|
||||
description:
|
||||
|
@ -1756,6 +1760,7 @@ export default {
|
|||
name: "dev",
|
||||
color: "000",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "dev",
|
||||
topic_count: 284,
|
||||
description:
|
||||
|
@ -1859,6 +1864,7 @@ export default {
|
|||
name: "ux",
|
||||
color: "5F497A",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "ux",
|
||||
topic_count: 184,
|
||||
description:
|
||||
|
@ -1960,6 +1966,7 @@ export default {
|
|||
name: "extensibility",
|
||||
color: "FE8432",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "extensibility",
|
||||
topic_count: 102,
|
||||
description:
|
||||
|
@ -2062,6 +2069,7 @@ export default {
|
|||
name: "hosting",
|
||||
color: "74CCED",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "hosting",
|
||||
topic_count: 69,
|
||||
description:
|
||||
|
@ -2163,6 +2171,7 @@ export default {
|
|||
name: "uncategorized",
|
||||
color: "0088CC",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "uncategorized",
|
||||
topic_count: 229,
|
||||
description: "",
|
||||
|
@ -2265,6 +2274,7 @@ export default {
|
|||
name: "login",
|
||||
color: "edb400",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "login",
|
||||
topic_count: 27,
|
||||
description:
|
||||
|
@ -2366,6 +2376,7 @@ export default {
|
|||
name: "meta",
|
||||
color: "aaa",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "meta",
|
||||
topic_count: 79,
|
||||
description:
|
||||
|
@ -2467,6 +2478,7 @@ export default {
|
|||
name: "discourse hub",
|
||||
color: "b2c79f",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "discourse-hub",
|
||||
topic_count: 4,
|
||||
description:
|
||||
|
@ -2567,6 +2579,7 @@ export default {
|
|||
name: "blog",
|
||||
color: "ED207B",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "blog",
|
||||
topic_count: 14,
|
||||
description:
|
||||
|
@ -2666,6 +2679,7 @@ export default {
|
|||
name: "faq",
|
||||
color: "33b",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "faq",
|
||||
topic_count: 49,
|
||||
description:
|
||||
|
@ -2769,6 +2783,7 @@ export default {
|
|||
name: "marketplace",
|
||||
color: "8C6238",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "marketplace",
|
||||
topic_count: 24,
|
||||
description:
|
||||
|
@ -2871,6 +2886,7 @@ export default {
|
|||
name: "howto",
|
||||
color: "76923C",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "howto",
|
||||
topic_count: 58,
|
||||
description:
|
||||
|
@ -6431,6 +6447,7 @@ export default {
|
|||
name: "Uncategorized",
|
||||
color: "0088CC",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "uncategorized",
|
||||
topic_count: 1,
|
||||
post_count: 0,
|
||||
|
@ -6458,6 +6475,7 @@ export default {
|
|||
name: "Site Feedback",
|
||||
color: "27AA5B",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "site-feedback",
|
||||
topic_count: 0,
|
||||
post_count: 0,
|
||||
|
|
|
@ -293,6 +293,7 @@ export default {
|
|||
description:
|
||||
"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 5823,
|
||||
|
@ -342,6 +343,7 @@ export default {
|
|||
description:
|
||||
"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 5823,
|
||||
|
@ -391,6 +393,7 @@ export default {
|
|||
description:
|
||||
"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 5823,
|
||||
|
@ -441,6 +444,7 @@ export default {
|
|||
description:
|
||||
"Discussion about features or potential features of Discourse: how they work, why they work, etc.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 14360,
|
||||
|
@ -491,6 +495,7 @@ export default {
|
|||
description:
|
||||
"Discussion about features or potential features of Discourse: how they work, why they work, etc.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 14360,
|
||||
|
@ -542,6 +547,7 @@ export default {
|
|||
description:
|
||||
"This category is for discussion about localizing Discourse.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 1167,
|
||||
|
@ -594,6 +600,7 @@ export default {
|
|||
description:
|
||||
"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 4196,
|
||||
|
@ -645,6 +652,7 @@ export default {
|
|||
description:
|
||||
"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 5823,
|
||||
|
@ -694,6 +702,7 @@ export default {
|
|||
description:
|
||||
"Discussion about features or potential features of Discourse: how they work, why they work, etc.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 14360,
|
||||
|
@ -744,6 +753,7 @@ export default {
|
|||
description:
|
||||
"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 12272,
|
||||
|
@ -793,6 +803,7 @@ export default {
|
|||
description:
|
||||
"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.",
|
||||
text_color: "000000",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 11179,
|
||||
|
@ -842,6 +853,7 @@ export default {
|
|||
description:
|
||||
"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 5823,
|
||||
|
@ -891,6 +903,7 @@ export default {
|
|||
description:
|
||||
"Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 1116,
|
||||
|
@ -940,6 +953,7 @@ export default {
|
|||
description:
|
||||
"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 12272,
|
||||
|
@ -989,6 +1003,7 @@ export default {
|
|||
description:
|
||||
"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.",
|
||||
text_color: "000000",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 11179,
|
||||
|
@ -1038,6 +1053,7 @@ export default {
|
|||
description:
|
||||
"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 12272,
|
||||
|
@ -1088,6 +1104,7 @@ export default {
|
|||
description:
|
||||
"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 4196,
|
||||
|
@ -1137,6 +1154,7 @@ export default {
|
|||
description:
|
||||
"Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility. ",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 2574,
|
||||
|
@ -1186,6 +1204,7 @@ export default {
|
|||
description:
|
||||
"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 12272,
|
||||
|
@ -1235,6 +1254,7 @@ export default {
|
|||
description:
|
||||
"Discussion about features or potential features of Discourse: how they work, why they work, etc.",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
read_restricted: false,
|
||||
auto_close_hours: null,
|
||||
post_count: 14360,
|
||||
|
|
|
@ -821,6 +821,7 @@ export default {
|
|||
name: "dev",
|
||||
color: "000",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "dev",
|
||||
topic_count: 701,
|
||||
post_count: 5320,
|
||||
|
|
|
@ -68,6 +68,7 @@ export default {
|
|||
name: "meta",
|
||||
color: "aaaaaa",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "meta",
|
||||
topic_count: 122,
|
||||
post_count: 1023,
|
||||
|
@ -89,6 +90,7 @@ export default {
|
|||
name: "howto",
|
||||
color: "76923C",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "howto",
|
||||
topic_count: 72,
|
||||
post_count: 1022,
|
||||
|
@ -109,6 +111,7 @@ export default {
|
|||
name: "spec",
|
||||
color: "33B0B0",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "spec",
|
||||
topic_count: 20,
|
||||
post_count: 278,
|
||||
|
@ -128,6 +131,7 @@ export default {
|
|||
name: "dev",
|
||||
color: "000",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "dev",
|
||||
topic_count: 481,
|
||||
post_count: 3575,
|
||||
|
@ -150,6 +154,7 @@ export default {
|
|||
name: "support",
|
||||
color: "b99",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "support",
|
||||
topic_count: 1603,
|
||||
post_count: 11075,
|
||||
|
@ -171,6 +176,7 @@ export default {
|
|||
name: "Shared Drafts",
|
||||
color: "92278F",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "shared-drafts",
|
||||
topic_count: 13,
|
||||
post_count: 53,
|
||||
|
@ -187,6 +193,7 @@ export default {
|
|||
name: "hack night",
|
||||
color: "B3B5B4",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "hack-night",
|
||||
topic_count: 8,
|
||||
post_count: 33,
|
||||
|
@ -206,6 +213,7 @@ export default {
|
|||
name: "translations",
|
||||
color: "27AA5B",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "translations",
|
||||
topic_count: 95,
|
||||
post_count: 827,
|
||||
|
@ -225,6 +233,7 @@ export default {
|
|||
name: "faq",
|
||||
color: "33b",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "faq",
|
||||
topic_count: 48,
|
||||
post_count: 501,
|
||||
|
@ -245,6 +254,7 @@ export default {
|
|||
name: "marketplace",
|
||||
color: "8C6238",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "marketplace",
|
||||
topic_count: 66,
|
||||
post_count: 361,
|
||||
|
@ -265,6 +275,7 @@ export default {
|
|||
name: "discourse hub",
|
||||
color: "b2c79f",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "discourse-hub",
|
||||
topic_count: 10,
|
||||
post_count: 164,
|
||||
|
@ -284,6 +295,7 @@ export default {
|
|||
name: "blog",
|
||||
color: "ED207B",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "blog",
|
||||
topic_count: 22,
|
||||
post_count: 390,
|
||||
|
@ -303,6 +315,7 @@ export default {
|
|||
name: "extensibility",
|
||||
color: "FE8432",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "extensibility",
|
||||
topic_count: 226,
|
||||
post_count: 1874,
|
||||
|
@ -323,6 +336,7 @@ export default {
|
|||
name: "login",
|
||||
color: "edb400",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "login",
|
||||
topic_count: 48,
|
||||
post_count: 357,
|
||||
|
@ -342,6 +356,7 @@ export default {
|
|||
name: "plugin",
|
||||
color: "d47711",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "plugin",
|
||||
topic_count: 40,
|
||||
post_count: 466,
|
||||
|
@ -360,6 +375,7 @@ export default {
|
|||
name: "bug",
|
||||
color: "e9dd00",
|
||||
text_color: "000000",
|
||||
style_type: "square",
|
||||
slug: "bug",
|
||||
topic_count: 1469,
|
||||
post_count: 9295,
|
||||
|
@ -381,6 +397,7 @@ export default {
|
|||
name: "uncategorized",
|
||||
color: "0088CC",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "uncategorized",
|
||||
topic_count: 342,
|
||||
post_count: 3090,
|
||||
|
@ -400,6 +417,7 @@ export default {
|
|||
name: "wordpress",
|
||||
color: "1E8CBE",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "wordpress",
|
||||
topic_count: 26,
|
||||
post_count: 135,
|
||||
|
@ -418,6 +436,7 @@ export default {
|
|||
name: "hosting",
|
||||
color: "74CCED",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "hosting",
|
||||
topic_count: 100,
|
||||
post_count: 917,
|
||||
|
@ -437,6 +456,7 @@ export default {
|
|||
name: "ux",
|
||||
color: "5F497A",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "ux",
|
||||
topic_count: 452,
|
||||
post_count: 4472,
|
||||
|
@ -456,6 +476,7 @@ export default {
|
|||
name: "feature",
|
||||
color: "0E76BD",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "feature",
|
||||
topic_count: 1367,
|
||||
post_count: 11942,
|
||||
|
@ -478,6 +499,7 @@ export default {
|
|||
name: "快乐的",
|
||||
color: "0E78BD",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "",
|
||||
topic_count: 137,
|
||||
post_count: 1142,
|
||||
|
@ -496,6 +518,7 @@ export default {
|
|||
name: "Restricted Group",
|
||||
color: "0E78BD",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "restricted-group",
|
||||
topic_count: 137,
|
||||
post_count: 1142,
|
||||
|
@ -514,6 +537,7 @@ export default {
|
|||
name: "Parent Category",
|
||||
color: "27AA5B",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "parent-category",
|
||||
topic_count: 95,
|
||||
post_count: 827,
|
||||
|
@ -533,6 +557,7 @@ export default {
|
|||
name: "Sub Category",
|
||||
color: "27AA5B",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "sub-category",
|
||||
topic_count: 95,
|
||||
post_count: 827,
|
||||
|
@ -551,6 +576,7 @@ export default {
|
|||
name: "Sub Sub Category",
|
||||
color: "27AA5B",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "sub-sub-category",
|
||||
topic_count: 95,
|
||||
post_count: 827,
|
||||
|
|
|
@ -260,6 +260,7 @@ export function applyDefaultHandlers(pretender) {
|
|||
name: "bug",
|
||||
color: "e9dd00",
|
||||
text_color: "000000",
|
||||
style_type: "square",
|
||||
slug: "bug",
|
||||
read_restricted: false,
|
||||
parent_category_id: null,
|
||||
|
|
|
@ -66,6 +66,7 @@ PreloadStore.store("site", {
|
|||
name: "extensibility",
|
||||
color: "FE8432",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "extensibility",
|
||||
topic_count: 102,
|
||||
description:
|
||||
|
@ -80,6 +81,7 @@ PreloadStore.store("site", {
|
|||
name: "dev",
|
||||
color: "000",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "dev",
|
||||
topic_count: 284,
|
||||
description:
|
||||
|
@ -94,6 +96,7 @@ PreloadStore.store("site", {
|
|||
name: "bug",
|
||||
color: "e9dd00",
|
||||
text_color: "000000",
|
||||
style_type: "square",
|
||||
slug: "bug",
|
||||
topic_count: 660,
|
||||
description:
|
||||
|
@ -108,6 +111,7 @@ PreloadStore.store("site", {
|
|||
name: "hosting",
|
||||
color: "74CCED",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "hosting",
|
||||
topic_count: 69,
|
||||
description:
|
||||
|
@ -122,6 +126,7 @@ PreloadStore.store("site", {
|
|||
name: "support",
|
||||
color: "b99",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "support",
|
||||
topic_count: 782,
|
||||
description:
|
||||
|
@ -136,6 +141,7 @@ PreloadStore.store("site", {
|
|||
name: "feature",
|
||||
color: "0E76BD",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "feature",
|
||||
topic_count: 727,
|
||||
description:
|
||||
|
@ -150,6 +156,7 @@ PreloadStore.store("site", {
|
|||
name: "blog",
|
||||
color: "ED207B",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "blog",
|
||||
topic_count: 14,
|
||||
description:
|
||||
|
@ -164,6 +171,7 @@ PreloadStore.store("site", {
|
|||
name: "discourse hub",
|
||||
color: "b2c79f",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "discourse-hub",
|
||||
topic_count: 4,
|
||||
description:
|
||||
|
@ -178,6 +186,7 @@ PreloadStore.store("site", {
|
|||
name: "login",
|
||||
color: "edb400",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "login",
|
||||
topic_count: 27,
|
||||
description:
|
||||
|
@ -192,6 +201,7 @@ PreloadStore.store("site", {
|
|||
name: "meta",
|
||||
color: "aaa",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "meta",
|
||||
topic_count: 79,
|
||||
description:
|
||||
|
@ -206,6 +216,7 @@ PreloadStore.store("site", {
|
|||
name: "howto",
|
||||
color: "76923C",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "howto",
|
||||
topic_count: 58,
|
||||
description:
|
||||
|
@ -220,6 +231,7 @@ PreloadStore.store("site", {
|
|||
name: "marketplace",
|
||||
color: "8C6238",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "marketplace",
|
||||
topic_count: 24,
|
||||
description:
|
||||
|
@ -234,6 +246,7 @@ PreloadStore.store("site", {
|
|||
name: "uncategorized",
|
||||
color: "0088CC",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "uncategorized",
|
||||
topic_count: 229,
|
||||
description: "",
|
||||
|
@ -247,6 +260,7 @@ PreloadStore.store("site", {
|
|||
name: "ux",
|
||||
color: "5F497A",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "ux",
|
||||
topic_count: 184,
|
||||
description:
|
||||
|
@ -261,6 +275,7 @@ PreloadStore.store("site", {
|
|||
name: "faq",
|
||||
color: "33b",
|
||||
text_color: "FFFFFF",
|
||||
style_type: "square",
|
||||
slug: "faq",
|
||||
topic_count: 49,
|
||||
description:
|
||||
|
|
|
@ -42,12 +42,14 @@ const MORE_COLLECTION = "MORE_COLLECTION";
|
|||
headerComponent: "category-drop/category-drop-header",
|
||||
parentCategory: false,
|
||||
allowUncategorized: "allowUncategorized",
|
||||
shouldDisplayIcon: "shouldDisplayIcon",
|
||||
})
|
||||
@pluginApiIdentifiers(["category-drop"])
|
||||
export default class CategoryDrop extends ComboBoxComponent {
|
||||
@readOnly("category.id") value;
|
||||
@readOnly("categoriesWithShortcuts.[]") content;
|
||||
@readOnly("selectKit.options.parentCategory.displayName") parentCategoryName;
|
||||
@readOnly("selectKit.options.shouldDisplayIcon") shouldDisplayIcon;
|
||||
@setting("allow_uncategorized_topics") allowUncategorized;
|
||||
|
||||
noCategoriesLabel = i18n("categories.no_subcategories");
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
tabindex=this.tabindex
|
||||
item=this.selectedContent
|
||||
selectKit=this.selectKit
|
||||
shouldDisplayIcon=this.shouldDisplayIcon
|
||||
shouldDisplayClearableButton=this.shouldDisplayClearableButton
|
||||
}}
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { reads } from "@ember/object/computed";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import ComboBoxSelectBoxHeaderComponent from "select-kit/components/combo-box/combo-box-header";
|
||||
|
||||
@classNames("category-drop-header")
|
||||
export default class CategoryDropHeader extends ComboBoxSelectBoxHeaderComponent {
|
||||
@reads("selectKit.options.shouldDisplayIcon") shouldDisplayIcon;
|
||||
|
||||
@discourseComputed("selectedContent.color")
|
||||
categoryBackgroundColor(categoryColor) {
|
||||
return categoryColor || "#e9e9e9";
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.item.icon}}
|
||||
{{#if (and this.renderIcon this.item.icon)}}
|
||||
{{d-icon this.item.icon}}
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -32,11 +32,17 @@ export default class SelectedName extends Component.extend(UtilsMixin) {
|
|||
headerTitle: this.getProperty(this.item, "titleProperty"),
|
||||
headerLang: this.getProperty(this.item, "langProperty"),
|
||||
name: this.getName(this.item),
|
||||
renderIcon: this.canDisplayIcon,
|
||||
value:
|
||||
this.item === this.selectKit.noneItem ? null : this.getValue(this.item),
|
||||
});
|
||||
}
|
||||
|
||||
@computed("selectKit.options.shouldDisplayIcon")
|
||||
get canDisplayIcon() {
|
||||
return this.selectKit.options.shouldDisplayIcon ?? true;
|
||||
}
|
||||
|
||||
@computed("item", "sanitizedTitle")
|
||||
get ariaLabel() {
|
||||
return this._safeProperty("ariaLabel", this.item) || this.sanitizedTitle;
|
||||
|
|
|
@ -107,6 +107,14 @@ div.edit-category {
|
|||
@include breakpoint("mobile-extra-large") {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.edit-category-tab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-category-tab.active {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
#list-area & h2 {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
--d-sidebar-section-link-prefix-margin-right: 0.75em;
|
||||
--d-sidebar-section-link-prefix-width: 1.35rem;
|
||||
--d-sidebar-section-link-icon-size: 0.8em;
|
||||
--d-sidebar-section-link-emoji-size: 0.9em;
|
||||
}
|
||||
|
||||
.sidebar-section-link-wrapper {
|
||||
|
@ -185,7 +186,8 @@
|
|||
}
|
||||
|
||||
&.icon,
|
||||
&.span {
|
||||
&.square,
|
||||
&.emoji {
|
||||
position: relative;
|
||||
color: var(--d-sidebar-link-icon-color);
|
||||
|
||||
|
@ -207,7 +209,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.prefix-span {
|
||||
&.emoji img {
|
||||
width: var(--d-sidebar-section-link-emoji-size);
|
||||
height: var(--d-sidebar-section-link-emoji-size);
|
||||
}
|
||||
|
||||
.prefix-square {
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
color: var(--primary-high);
|
||||
min-width: 0;
|
||||
|
||||
&::before {
|
||||
&.--style-square::before {
|
||||
content: "";
|
||||
background: var(--category-badge-color);
|
||||
flex: 0 0 auto;
|
||||
|
@ -39,6 +39,22 @@
|
|||
height: 0.625rem;
|
||||
}
|
||||
|
||||
&.--style-emoji,
|
||||
&.--style-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.d-icon,
|
||||
.emoji {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&.--style-icon .d-icon {
|
||||
color: var(--category-badge-color);
|
||||
}
|
||||
|
||||
&__name {
|
||||
color: currentcolor;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
.add-on {
|
||||
@include form-item-sizing;
|
||||
background-color: var(--primary-low);
|
||||
border-color: var(--primary-medium);
|
||||
border-color: var(--primary-low-mid);
|
||||
border-right-color: transparent;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
|
|
|
@ -31,8 +31,8 @@ a.hashtag {
|
|||
|
||||
.d-icon,
|
||||
.hashtag-icon-placeholder {
|
||||
font-size: var(--font-down-2);
|
||||
margin: 0 0.3em 0 0;
|
||||
font-size: var(--font-down-1);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
|
@ -45,7 +45,7 @@ a.hashtag {
|
|||
display: inline;
|
||||
}
|
||||
|
||||
.hashtag-category-badge {
|
||||
.hashtag-category-square {
|
||||
flex: 0 0 auto;
|
||||
width: 0.72em;
|
||||
height: 0.72em;
|
||||
|
@ -53,6 +53,12 @@ a.hashtag {
|
|||
margin-left: 0.1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hashtag-category-icon,
|
||||
.hashtag-category-emoji {
|
||||
margin-right: 0.25em;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.hashtag-autocomplete {
|
||||
|
@ -101,7 +107,7 @@ a.hashtag {
|
|||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.hashtag-category-badge {
|
||||
.hashtag-category-square {
|
||||
flex: 0 0 auto;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
font-size: var(--font-down-1-rem);
|
||||
color: var(--primary-high);
|
||||
padding-bottom: 0.25em;
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-optional {
|
||||
|
|
|
@ -546,6 +546,9 @@ class CategoriesController < ApplicationController
|
|||
:name,
|
||||
:color,
|
||||
:text_color,
|
||||
:style_type,
|
||||
:emoji,
|
||||
:icon,
|
||||
:email_in,
|
||||
:email_in_allow_strangers,
|
||||
:mailinglist_mirror,
|
||||
|
|
|
@ -232,6 +232,8 @@ class Category < ActiveRecord::Base
|
|||
# Allows us to skip creating the category definition topic in tests.
|
||||
attr_accessor :skip_category_definition
|
||||
|
||||
enum :style_type, { square: 0, icon: 1, emoji: 2 }
|
||||
|
||||
def self.preload_user_fields!(guardian, categories)
|
||||
category_ids = categories.map(&:id)
|
||||
|
||||
|
@ -1381,6 +1383,9 @@ end
|
|||
# default_slow_mode_seconds :integer
|
||||
# uploaded_logo_dark_id :integer
|
||||
# uploaded_background_dark_id :integer
|
||||
# style_type :integer default("square"), not null
|
||||
# emoji :string
|
||||
# icon :string
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -130,7 +130,18 @@ class UserSummary
|
|||
|
||||
class CategoryWithCounts < OpenStruct
|
||||
include ActiveModel::SerializerSupport
|
||||
KEYS = %i[id name color text_color slug read_restricted parent_category_id]
|
||||
KEYS = %i[
|
||||
id
|
||||
name
|
||||
color
|
||||
text_color
|
||||
style_type
|
||||
icon
|
||||
emoji
|
||||
slug
|
||||
read_restricted
|
||||
parent_category_id
|
||||
]
|
||||
end
|
||||
|
||||
def top_categories
|
||||
|
@ -142,7 +153,18 @@ class UserSummary
|
|||
.where(
|
||||
id: post_count_query.order("count(*) DESC").limit(MAX_SUMMARY_RESULTS).pluck("category_id"),
|
||||
)
|
||||
.pluck(:id, :name, :color, :text_color, :slug, :read_restricted, :parent_category_id)
|
||||
.pluck(
|
||||
:id,
|
||||
:name,
|
||||
:color,
|
||||
:text_color,
|
||||
:style_type,
|
||||
:icon,
|
||||
:emoji,
|
||||
:slug,
|
||||
:read_restricted,
|
||||
:parent_category_id,
|
||||
)
|
||||
.each do |c|
|
||||
top_categories[c[0].to_i] = CategoryWithCounts.new(
|
||||
Hash[CategoryWithCounts::KEYS.zip(c)].merge(topic_count: 0, post_count: 0),
|
||||
|
|
|
@ -5,6 +5,9 @@ class BasicCategorySerializer < ApplicationSerializer
|
|||
:name,
|
||||
:color,
|
||||
:text_color,
|
||||
:style_type,
|
||||
:icon,
|
||||
:emoji,
|
||||
:slug,
|
||||
:topic_count,
|
||||
:post_count,
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CategoryBadgeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :slug, :color, :text_color, :read_restricted, :parent_category_id
|
||||
attributes :id,
|
||||
:name,
|
||||
:slug,
|
||||
:color,
|
||||
:text_color,
|
||||
:style_type,
|
||||
:icon,
|
||||
:emoji,
|
||||
:read_restricted,
|
||||
:parent_category_id
|
||||
|
||||
def include_parent_category_id?
|
||||
parent_category_id.present?
|
||||
|
|
|
@ -27,7 +27,10 @@ class CategorySerializer < SiteCategorySerializer
|
|||
:topic_featured_link_allowed,
|
||||
:search_priority,
|
||||
:moderating_group_ids,
|
||||
:default_slow_mode_seconds
|
||||
:default_slow_mode_seconds,
|
||||
:style_type,
|
||||
:emoji,
|
||||
:icon
|
||||
|
||||
has_one :category_setting, serializer: CategorySettingSerializer, embed: :objects
|
||||
|
||||
|
|
|
@ -70,6 +70,9 @@ class UserSummarySerializer < ApplicationSerializer
|
|||
:name,
|
||||
:color,
|
||||
:text_color,
|
||||
:style_type,
|
||||
:icon,
|
||||
:emoji,
|
||||
:slug,
|
||||
:read_restricted,
|
||||
:parent_category_id
|
||||
|
|
|
@ -28,10 +28,12 @@ class CategoryHashtagDataSource
|
|||
)
|
||||
item.slug = category.slug
|
||||
item.description = category.description_text
|
||||
item.icon = icon
|
||||
item.colors = [category.parent_category&.color, category.color].compact
|
||||
item.relative_url = category.url
|
||||
item.id = category.id
|
||||
item.style_type = category.style_type
|
||||
item.icon = category.style_type == "icon" ? category.icon : icon
|
||||
item.emoji = category.emoji if category.style_type == "emoji"
|
||||
|
||||
# Single-level category hierarchy should be enough to distinguish between
|
||||
# categories here.
|
||||
|
@ -63,7 +65,17 @@ class CategoryHashtagDataSource
|
|||
base_search =
|
||||
Category
|
||||
.secured(guardian)
|
||||
.select(:id, :parent_category_id, :slug, :name, :description, :color)
|
||||
.select(
|
||||
:id,
|
||||
:parent_category_id,
|
||||
:slug,
|
||||
:name,
|
||||
:description,
|
||||
:color,
|
||||
:style_type,
|
||||
:icon,
|
||||
:emoji,
|
||||
)
|
||||
.includes(:parent_category)
|
||||
|
||||
if condition == HashtagAutocompleteService.search_conditions[:starts_with]
|
||||
|
|
|
@ -82,6 +82,12 @@ class HashtagAutocompleteService
|
|||
# have the type as a suffix to distinguish between conflicts.
|
||||
attr_accessor :slug
|
||||
|
||||
# Display style for the item, e.g. square, icon, emoji
|
||||
attr_accessor :style_type
|
||||
|
||||
# The emoji to display in the UI autocomplete menu (without colons)
|
||||
attr_accessor :emoji
|
||||
|
||||
# The icon to display in the UI autocomplete menu for the item.
|
||||
attr_accessor :icon
|
||||
|
||||
|
@ -108,7 +114,9 @@ class HashtagAutocompleteService
|
|||
@relative_url = params[:relative_url]
|
||||
@text = params[:text]
|
||||
@description = params[:description]
|
||||
@style_type = params[:style_type]
|
||||
@icon = params[:icon]
|
||||
@emoji = params[:emoji]
|
||||
@colors = params[:colors]
|
||||
@type = params[:type]
|
||||
@ref = params[:ref]
|
||||
|
@ -117,7 +125,7 @@ class HashtagAutocompleteService
|
|||
end
|
||||
|
||||
def to_h
|
||||
{
|
||||
opts = {
|
||||
relative_url: self.relative_url,
|
||||
text: self.text,
|
||||
description: self.description,
|
||||
|
@ -128,6 +136,13 @@ class HashtagAutocompleteService
|
|||
slug: self.slug,
|
||||
id: self.id,
|
||||
}
|
||||
|
||||
if self.style_type.present?
|
||||
opts[:style_type] = self.style_type
|
||||
opts[:emoji] = self.emoji
|
||||
end
|
||||
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4126,15 +4126,24 @@ en:
|
|||
creation_error: There has been an error during the creation of the category.
|
||||
save_error: There was an error saving the category.
|
||||
name: "Category Name"
|
||||
untitled: "Untitled Category"
|
||||
description: "Description"
|
||||
logo: "Category Logo Image"
|
||||
logo_dark: "Dark Mode Category Logo Image"
|
||||
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"
|
||||
badge_colors: "Badge colors"
|
||||
background_color: "Background color"
|
||||
style: "Styles"
|
||||
background_color: "Color"
|
||||
foreground_color: "Foreground color"
|
||||
styles:
|
||||
type: "Style"
|
||||
icon: "Icon"
|
||||
emoji: "Emoji"
|
||||
options:
|
||||
square: "Square"
|
||||
icon: "Icon"
|
||||
emoji: "Emoji"
|
||||
color_used: "Color in use"
|
||||
predefined_colors: "Predefined color options"
|
||||
name_placeholder: "One or two words maximum"
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
class AddStyleTypeToCategories < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :categories, :style_type, :integer, default: 0, null: false
|
||||
add_column :categories, :emoji, :string
|
||||
add_column :categories, :icon, :string
|
||||
end
|
||||
end
|
|
@ -13,6 +13,9 @@ module ImportExport
|
|||
slug
|
||||
description
|
||||
text_color
|
||||
style_type
|
||||
icon
|
||||
emoji
|
||||
auto_close_hours
|
||||
position
|
||||
parent_category_id
|
||||
|
|
|
@ -152,6 +152,8 @@ task "javascript:update_constants" => :environment do
|
|||
max_title_length: #{SidebarSection::MAX_TITLE_LENGTH},
|
||||
}
|
||||
|
||||
export const CATEGORY_STYLE_TYPES = #{Category.style_types.to_json};
|
||||
|
||||
export const AUTO_GROUPS = #{auto_groups.to_json};
|
||||
|
||||
export const GROUP_SMTP_SSL_MODES = #{Group.smtp_ssl_modes.to_json};
|
||||
|
|
|
@ -102,7 +102,7 @@ describe "Using #hashtag autocompletion to search for and lookup channels", type
|
|||
with_tag(
|
||||
"span",
|
||||
with: {
|
||||
class: "hashtag-category-badge hashtag-color--category-#{category.id}",
|
||||
class: "hashtag-category-square hashtag-color--category-#{category.id}",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
|
|
@ -3,11 +3,22 @@ import { test } from "qunit";
|
|||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
acceptance("Chat | Hashtag CSS Generator", function (needs) {
|
||||
const category1 = { id: 1, color: "ff0000", name: "category1" };
|
||||
const category2 = { id: 2, color: "333", name: "category2" };
|
||||
const category1 = {
|
||||
id: 1,
|
||||
color: "ff0000",
|
||||
style_type: "square",
|
||||
name: "category1",
|
||||
};
|
||||
const category2 = {
|
||||
id: 2,
|
||||
color: "333",
|
||||
style_type: "square",
|
||||
name: "category2",
|
||||
};
|
||||
const category3 = {
|
||||
id: 4,
|
||||
color: "2B81AF",
|
||||
style_type: "square",
|
||||
parent_category_id: 1,
|
||||
name: "category3",
|
||||
};
|
||||
|
@ -68,7 +79,7 @@ acceptance("Chat | Hashtag CSS Generator", function (needs) {
|
|||
assert
|
||||
.dom("style#hashtag-css-generator", document.head)
|
||||
.hasHtml(
|
||||
".hashtag-category-badge { background-color: var(--primary-medium); }\n" +
|
||||
".hashtag-category-square { background-color: var(--primary-medium); }\n" +
|
||||
".hashtag-color--category-1 { background-color: #ff0000; }\n" +
|
||||
".hashtag-color--category-2 { background-color: #333; }\n" +
|
||||
".hashtag-color--category-4 { background: linear-gradient(-90deg, #2B81AF 50%, #ff0000 50%); }"
|
||||
|
|
|
@ -592,7 +592,7 @@ RSpec.describe Email::Sender do
|
|||
reply.rebake!
|
||||
Email::Sender.new(message, :valid_type).send
|
||||
expected = <<~HTML
|
||||
<a href=\"#{Discourse.base_url}#{category.url}\" data-type=\"category\" data-slug=\"dev\" data-id=\"#{category.id}\" style=\"text-decoration:none;font-weight:bold;color:#006699\"><span>#dev</span>
|
||||
<a href=\"#{Discourse.base_url}#{category.url}\" data-type=\"category\" data-slug=\"dev\" data-id=\"#{category.id}\" data-style-type=\"square\" style=\"text-decoration:none;font-weight:bold;color:#006699\"><span>#dev</span>
|
||||
HTML
|
||||
expect(message.html_part.body.to_s).to include(expected.chomp)
|
||||
end
|
||||
|
|
|
@ -55,8 +55,10 @@ RSpec.describe PrettyText::Helpers do
|
|||
relative_url: category.url,
|
||||
text: "Some Awesome Category",
|
||||
description: "Really great stuff here",
|
||||
colors: [category.color],
|
||||
style_type: "square",
|
||||
emoji: nil,
|
||||
icon: "folder",
|
||||
colors: [category.color],
|
||||
id: category.id,
|
||||
slug: "someawesomecategory",
|
||||
ref: "someawesomecategory::category",
|
||||
|
@ -74,6 +76,8 @@ RSpec.describe PrettyText::Helpers do
|
|||
text: "Some Awesome Category",
|
||||
description: "Really great stuff here",
|
||||
colors: [category.color],
|
||||
style_type: "square",
|
||||
emoji: nil,
|
||||
icon: "folder",
|
||||
id: category.id,
|
||||
slug: "someawesomecategory",
|
||||
|
@ -105,6 +109,8 @@ RSpec.describe PrettyText::Helpers do
|
|||
text: "Some Awesome Category",
|
||||
description: "Really great stuff here",
|
||||
colors: [category.color],
|
||||
style_type: "square",
|
||||
emoji: nil,
|
||||
icon: "folder",
|
||||
id: category.id,
|
||||
slug: "someawesomecategory",
|
||||
|
@ -128,8 +134,10 @@ RSpec.describe PrettyText::Helpers do
|
|||
relative_url: private_category.url,
|
||||
text: "Manager Hideout",
|
||||
description: nil,
|
||||
colors: [private_category.color],
|
||||
style_type: "square",
|
||||
emoji: nil,
|
||||
icon: "folder",
|
||||
colors: [private_category.color],
|
||||
id: private_category.id,
|
||||
slug: "secretcategory",
|
||||
ref: "secretcategory",
|
||||
|
|
|
@ -1814,7 +1814,7 @@ RSpec.describe PrettyText do
|
|||
it "produces hashtag links" do
|
||||
user = Fabricate(:user)
|
||||
category = Fabricate(:category, name: "testing", slug: "testing")
|
||||
category2 = Fabricate(:category, name: "known", slug: "known")
|
||||
category2 = Fabricate(:category, name: "known", slug: "known", style_type: "icon", icon: "book")
|
||||
group = Fabricate(:group)
|
||||
private_category = Fabricate(:private_category, name: "secret", group: group, slug: "secret")
|
||||
tag = Fabricate(:tag, name: "known")
|
||||
|
@ -1831,6 +1831,8 @@ RSpec.describe PrettyText do
|
|||
"data-type": "category",
|
||||
"data-slug": category2.slug,
|
||||
"data-id": category2.id,
|
||||
"data-style-type": category2.style_type,
|
||||
"data-icon": category2.icon,
|
||||
},
|
||||
) do
|
||||
with_tag("span", with: { class: "hashtag-icon-placeholder" })
|
||||
|
|
|
@ -12,6 +12,15 @@
|
|||
"type": "string",
|
||||
"example": "f0fcfd"
|
||||
},
|
||||
"style_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"emoji": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_category_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
|
|
@ -17,6 +17,21 @@
|
|||
"text_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"style_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"emoji": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -279,6 +294,9 @@
|
|||
"name",
|
||||
"color",
|
||||
"text_color",
|
||||
"style_type",
|
||||
"emoji",
|
||||
"icon",
|
||||
"slug",
|
||||
"topic_count",
|
||||
"post_count",
|
||||
|
|
|
@ -30,6 +30,15 @@
|
|||
"text_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"style_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"emoji": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"icon": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -186,6 +195,9 @@
|
|||
"name",
|
||||
"color",
|
||||
"text_color",
|
||||
"style_type",
|
||||
"emoji",
|
||||
"icon",
|
||||
"slug",
|
||||
"topic_count",
|
||||
"post_count",
|
||||
|
|
|
@ -20,6 +20,21 @@
|
|||
"text_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"style_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"emoji": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
@ -606,6 +606,21 @@
|
|||
"text_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"style_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"emoji": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
@ -34,6 +34,8 @@ RSpec.describe HashtagsController do
|
|||
"text" => category.name,
|
||||
"description" => nil,
|
||||
"colors" => [category.color],
|
||||
"style_type" => "square",
|
||||
"emoji" => nil,
|
||||
"icon" => "folder",
|
||||
"type" => "category",
|
||||
"ref" => category.slug,
|
||||
|
@ -69,6 +71,8 @@ RSpec.describe HashtagsController do
|
|||
"text" => category.name,
|
||||
"description" => nil,
|
||||
"colors" => [category.color],
|
||||
"style_type" => "square",
|
||||
"emoji" => nil,
|
||||
"icon" => "folder",
|
||||
"type" => "category",
|
||||
"ref" => category.slug,
|
||||
|
@ -94,6 +98,8 @@ RSpec.describe HashtagsController do
|
|||
"text" => category.name,
|
||||
"description" => nil,
|
||||
"colors" => [category.color],
|
||||
"style_type" => "square",
|
||||
"emoji" => nil,
|
||||
"icon" => "folder",
|
||||
"type" => "category",
|
||||
"ref" => category.slug,
|
||||
|
@ -105,8 +111,10 @@ RSpec.describe HashtagsController do
|
|||
"text" => private_category.name,
|
||||
"description" => nil,
|
||||
"colors" => [private_category.color],
|
||||
"icon" => "folder",
|
||||
"type" => "category",
|
||||
"style_type" => "square",
|
||||
"emoji" => nil,
|
||||
"icon" => "folder",
|
||||
"ref" => private_category.slug,
|
||||
"slug" => private_category.slug,
|
||||
"id" => private_category.id,
|
||||
|
@ -129,8 +137,10 @@ RSpec.describe HashtagsController do
|
|||
"text" => category.name,
|
||||
"description" => nil,
|
||||
"colors" => [category.color],
|
||||
"icon" => "folder",
|
||||
"type" => "category",
|
||||
"style_type" => "square",
|
||||
"icon" => "folder",
|
||||
"emoji" => nil,
|
||||
"ref" => category.slug,
|
||||
"slug" => category.slug,
|
||||
"id" => category.id,
|
||||
|
@ -162,8 +172,10 @@ RSpec.describe HashtagsController do
|
|||
"relative_url" => category.url,
|
||||
"text" => category.name,
|
||||
"description" => nil,
|
||||
"colors" => [category.color],
|
||||
"style_type" => "square",
|
||||
"emoji" => nil,
|
||||
"icon" => "folder",
|
||||
"colors" => [category.color],
|
||||
"type" => "category",
|
||||
"ref" => category.slug,
|
||||
"slug" => category.slug,
|
||||
|
@ -176,6 +188,8 @@ RSpec.describe HashtagsController do
|
|||
"text" => tag.name,
|
||||
"description" => nil,
|
||||
"colors" => nil,
|
||||
"style_type" => nil,
|
||||
"emoji" => nil,
|
||||
"icon" => "tag",
|
||||
"type" => "tag",
|
||||
"ref" => tag.name,
|
||||
|
@ -201,6 +215,8 @@ RSpec.describe HashtagsController do
|
|||
"text" => tag.name,
|
||||
"description" => nil,
|
||||
"colors" => nil,
|
||||
"style_type" => nil,
|
||||
"emoji" => nil,
|
||||
"icon" => "tag",
|
||||
"type" => "tag",
|
||||
"ref" => "#{tag.name}::tag",
|
||||
|
@ -247,8 +263,10 @@ RSpec.describe HashtagsController do
|
|||
"relative_url" => private_category.url,
|
||||
"text" => private_category.name,
|
||||
"description" => nil,
|
||||
"colors" => [private_category.color],
|
||||
"style_type" => "square",
|
||||
"emoji" => nil,
|
||||
"icon" => "folder",
|
||||
"colors" => [private_category.color],
|
||||
"type" => "category",
|
||||
"ref" => private_category.slug,
|
||||
"slug" => private_category.slug,
|
||||
|
@ -261,6 +279,8 @@ RSpec.describe HashtagsController do
|
|||
"text" => hidden_tag.name,
|
||||
"description" => nil,
|
||||
"colors" => nil,
|
||||
"style_type" => nil,
|
||||
"emoji" => nil,
|
||||
"icon" => "tag",
|
||||
"type" => "tag",
|
||||
"ref" => hidden_tag.name,
|
||||
|
@ -345,6 +365,8 @@ RSpec.describe HashtagsController do
|
|||
"relative_url" => category.url,
|
||||
"text" => category.name,
|
||||
"description" => nil,
|
||||
"style_type" => "square",
|
||||
"emoji" => nil,
|
||||
"icon" => "folder",
|
||||
"colors" => [category.color],
|
||||
"type" => "category",
|
||||
|
@ -357,6 +379,8 @@ RSpec.describe HashtagsController do
|
|||
"text" => tag_2.name,
|
||||
"description" => nil,
|
||||
"colors" => nil,
|
||||
"style_type" => nil,
|
||||
"emoji" => nil,
|
||||
"icon" => "tag",
|
||||
"type" => "tag",
|
||||
"ref" => "#{tag_2.name}::tag",
|
||||
|
@ -391,8 +415,10 @@ RSpec.describe HashtagsController do
|
|||
"relative_url" => private_category.url,
|
||||
"text" => private_category.name,
|
||||
"description" => nil,
|
||||
"colors" => [private_category.color],
|
||||
"style_type" => "square",
|
||||
"emoji" => nil,
|
||||
"icon" => "folder",
|
||||
"colors" => [private_category.color],
|
||||
"type" => "category",
|
||||
"ref" => private_category.slug,
|
||||
"slug" => private_category.slug,
|
||||
|
@ -409,8 +435,10 @@ RSpec.describe HashtagsController do
|
|||
"relative_url" => hidden_tag.url,
|
||||
"text" => hidden_tag.name,
|
||||
"description" => nil,
|
||||
"colors" => nil,
|
||||
"style_type" => nil,
|
||||
"emoji" => nil,
|
||||
"icon" => "tag",
|
||||
"colors" => nil,
|
||||
"type" => "tag",
|
||||
"ref" => "#{hidden_tag.name}",
|
||||
"slug" => hidden_tag.name,
|
||||
|
|
|
@ -70,7 +70,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
|||
with_tag(
|
||||
"span",
|
||||
with: {
|
||||
class: "hashtag-category-badge hashtag-color--category-#{category.id}",
|
||||
class: "hashtag-category-square hashtag-color--category-#{category.id}",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
@ -124,7 +124,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
|||
with_tag(
|
||||
"span",
|
||||
with: {
|
||||
class: "hashtag-category-badge hashtag-color--category-#{category.id}",
|
||||
class: "hashtag-category-square hashtag-color--category-#{category.id}",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
@ -186,7 +186,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
|||
with_tag(
|
||||
"span",
|
||||
with: {
|
||||
class: "hashtag-category-badge hashtag-color--category-#{category.id}",
|
||||
class: "hashtag-category-square hashtag-color--category-#{category.id}",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
@ -218,7 +218,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
|||
with_tag(
|
||||
"span",
|
||||
with: {
|
||||
class: "hashtag-category-badge hashtag-color--category-#{category.id}",
|
||||
class: "hashtag-category-square hashtag-color--category-#{category.id}",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
@ -255,9 +255,9 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
|||
|
||||
it "shows a default color and css class for the category icon square" do
|
||||
topic_page.visit_topic(topic, post_number: post_with_private_category.post_number)
|
||||
expect(page).to have_css(".hashtag-cooked .hashtag-category-badge")
|
||||
expect(page).to have_css(".hashtag-cooked .hashtag-category-square")
|
||||
generated_css = find("#hashtag-css-generator", visible: false).text(:all)
|
||||
expect(generated_css).to include(".hashtag-category-badge")
|
||||
expect(generated_css).to include(".hashtag-category-square")
|
||||
expect(generated_css).not_to include(".hashtag-color--category--#{private_category.id}")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,7 @@ describe "New Category", type: :system do
|
|||
category_page.find(".edit-category-tab-general input.category-name").fill_in(
|
||||
with: "New Category",
|
||||
)
|
||||
|
||||
category_page.find(".edit-category-nav .edit-category-tags a").click
|
||||
category_page.find(".edit-category-tab-tags #category-minimum-tags").click
|
||||
category_page.save_settings
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue