2
0
Fork 0
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:
David Battersby 2025-03-26 09:46:17 +04:00 committed by GitHub
parent 1fd553ccb0
commit d06c60ca7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1036 additions and 361 deletions

View file

@ -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]",
]);
}

View file

@ -165,6 +165,7 @@ export default class BreadCrumbs extends Component {
subCategory=breadcrumb.isSubcategory
noSubcategories=breadcrumb.noSubcategories
autoFilterable=true
shouldDisplayIcon=false
}}
/>
</li>

View file

@ -13,6 +13,7 @@ export default class ColorPicker extends Component {
@action
selectColor(color) {
this.set("value", color);
this.onSelectColor?.(color);
}
@action

View file

@ -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>
}

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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}}

View file

@ -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);

View file

@ -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>
}

View file

@ -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)

View file

@ -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|
>

View file

@ -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(

View file

@ -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,

View file

@ -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);
})

View file

@ -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]);
}

View file

@ -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) {

View file

@ -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 {

View file

@ -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",
});

View file

@ -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>

View file

@ -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");

View file

@ -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();

View file

@ -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%); }"

View file

@ -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"

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -821,6 +821,7 @@ export default {
name: "dev",
color: "000",
text_color: "FFFFFF",
style_type: "square",
slug: "dev",
topic_count: 701,
post_count: 5320,

View file

@ -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,

View file

@ -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,

View file

@ -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:

View file

@ -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");

View file

@ -4,6 +4,7 @@
tabindex=this.tabindex
item=this.selectedContent
selectKit=this.selectKit
shouldDisplayIcon=this.shouldDisplayIcon
shouldDisplayClearableButton=this.shouldDisplayClearableButton
}}

View file

@ -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";

View file

@ -14,7 +14,7 @@
/>
{{/if}}
{{#if this.item.icon}}
{{#if (and this.renderIcon this.item.icon)}}
{{d-icon this.item.icon}}
{{/if}}

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -28,6 +28,10 @@
font-size: var(--font-down-1-rem);
color: var(--primary-high);
padding-bottom: 0.25em;
> * {
margin: 0;
}
}
&-optional {

View file

@ -546,6 +546,9 @@ class CategoriesController < ApplicationController
:name,
:color,
:text_color,
:style_type,
:emoji,
:icon,
:email_in,
:email_in_allow_strangers,
:mailinglist_mirror,

View file

@ -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
#

View file

@ -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),

View file

@ -5,6 +5,9 @@ class BasicCategorySerializer < ApplicationSerializer
:name,
:color,
:text_color,
:style_type,
:icon,
:emoji,
:slug,
:topic_count,
:post_count,

View file

@ -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?

View file

@ -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

View file

@ -70,6 +70,9 @@ class UserSummarySerializer < ApplicationSerializer
:name,
:color,
:text_color,
:style_type,
:icon,
:emoji,
:slug,
:read_restricted,
:parent_category_id

View file

@ -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]

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -13,6 +13,9 @@ module ImportExport
slug
description
text_color
style_type
icon
emoji
auto_close_hours
position
parent_category_id

View file

@ -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};

View file

@ -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

View file

@ -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%); }"

View file

@ -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

View file

@ -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",

View file

@ -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" })

View file

@ -12,6 +12,15 @@
"type": "string",
"example": "f0fcfd"
},
"style_type": {
"type": "string"
},
"emoji": {
"type": "string"
},
"icon": {
"type": "string"
},
"parent_category_id": {
"type": "integer"
},

View file

@ -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",

View file

@ -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",

View file

@ -20,6 +20,21 @@
"text_color": {
"type": "string"
},
"style_type": {
"type": "string"
},
"emoji": {
"type": [
"string",
"null"
]
},
"icon": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},

View file

@ -606,6 +606,21 @@
"text_color": {
"type": "string"
},
"style_type": {
"type": "string"
},
"emoji": {
"type": [
"string",
"null"
]
},
"icon": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string"
},

View file

@ -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,

View file

@ -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

View file

@ -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