DEV: code improvements to upsert category (#37572)

Follow-up to #37540. This continues the code cleanup work for the
upsert-category components.

Changes:
- consolidate 8 upload actions into a single pair using fn partial
application to reduce repetition
- move appearance-related properties (views, sorts, periods, filters,
subcategory list styles) from settings to appearance where they actually
belong
- use site.periods as the single source of truth for top periods instead
of hardcoding values
- add none option to ComboBoxes so values can be cleared back to default
- remove enable_simplified_category_creation guard from text_color and
slug fields since they should always be available
- remove unnecessary placementStrategy="absolute" from ComboBoxes
- add missing "hot" to available default views
- pass form to category-email-in plugin outlet so plugins can use it
- rename properties for consistency (eg. availableSorts → sortOrders,
parentIsRestricted → isParentRestricted)

Ref - t/173336
This commit is contained in:
Régis Hanol
2026-02-05 19:41:53 +01:00
committed by GitHub
parent e2dc31b2cf
commit e09cf0b1f2
3 changed files with 78 additions and 219 deletions
@@ -1,6 +1,5 @@
import Component from "@glimmer/component";
import { cached } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { fn, hash } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import PluginOutlet from "discourse/components/plugin-outlet";
@@ -14,7 +13,7 @@ import { eq } from "discourse/truth-helpers";
import { i18n } from "discourse-i18n";
export default class UpsertCategoryAppearance extends Component {
@service siteSettings;
@service site;
get isDefaultSortOrder() {
return !this.args.transientData?.sort_order;
@@ -36,7 +35,7 @@ export default class UpsertCategoryAppearance extends Component {
return this.args.transientData?.uploaded_logo?.url ?? "";
}
get logoImageDarkUrl() {
get logoDarkImageUrl() {
return this.args.transientData?.uploaded_logo_dark?.url ?? "";
}
@@ -48,52 +47,13 @@ export default class UpsertCategoryAppearance extends Component {
}
@action
logoUploadDone(upload) {
this.args.form.set("uploaded_logo", { url: upload.url, id: upload.id });
onUploadDone(field, upload) {
this.args.form.set(field, { url: upload.url, id: upload.id });
}
@action
logoUploadDeleted() {
this.args.form.set("uploaded_logo", { id: null, url: null });
}
@action
logoDarkUploadDone(upload) {
this.args.form.set("uploaded_logo_dark", {
url: upload.url,
id: upload.id,
});
}
@action
logoDarkUploadDeleted() {
this.args.form.set("uploaded_logo_dark", { id: null, url: null });
}
@action
backgroundUploadDone(upload) {
this.args.form.set("uploaded_background", {
url: upload.url,
id: upload.id,
});
}
@action
backgroundUploadDeleted() {
this.args.form.set("uploaded_background", { id: null, url: null });
}
@action
backgroundDarkUploadDone(upload) {
this.args.form.set("uploaded_background_dark", {
url: upload.url,
id: upload.id,
});
}
@action
backgroundDarkUploadDeleted() {
this.args.form.set("uploaded_background_dark", { id: null, url: null });
onUploadDeleted(field) {
this.args.form.set(field, { id: null, url: null });
}
@action
@@ -101,8 +61,7 @@ export default class UpsertCategoryAppearance extends Component {
this.args.form.set("sort_ascending", value);
}
@cached
get availableSubcategoryListStyles() {
get subcategoryListStyles() {
return [
{ name: i18n("category.subcategory_list_styles.rows"), value: "rows" },
{
@@ -124,12 +83,11 @@ export default class UpsertCategoryAppearance extends Component {
];
}
@cached
get availableViews() {
const views = [
{ name: i18n("filters.latest.title"), value: "latest" },
{ name: i18n("filters.top.title"), value: "top" },
];
const views = ["hot", "latest", "top"].map((value) => ({
name: i18n(`filters.${value}.title`),
value,
}));
const context = {
categoryId: this.args.category.id,
@@ -143,21 +101,21 @@ export default class UpsertCategoryAppearance extends Component {
);
}
get availableTopPeriods() {
return ["all", "yearly", "quarterly", "monthly", "weekly", "daily"].map(
(p) => {
return { name: i18n(`filters.top.${p}.title`), value: p };
}
);
get topPeriods() {
return this.site.periods.map((value) => ({
name: i18n(`filters.top.${value}.title`),
value,
}));
}
get availableListFilters() {
return ["all", "none"].map((p) => {
return { name: i18n(`category.list_filters.${p}`), value: p };
});
get listFilters() {
return ["all", "none"].map((value) => ({
name: i18n(`category.list_filters.${value}`),
value,
}));
}
get availableSorts() {
get sortOrders() {
return applyMutableValueTransformer("category-sort-orders", [
"likes",
"op_likes",
@@ -204,8 +162,8 @@ export default class UpsertCategoryAppearance extends Component {
>
<UppyImageUploader
@imageUrl={{this.logoImageUrl}}
@onUploadDone={{this.logoUploadDone}}
@onUploadDeleted={{this.logoUploadDeleted}}
@onUploadDone={{fn this.onUploadDone "uploaded_logo"}}
@onUploadDeleted={{fn this.onUploadDeleted "uploaded_logo"}}
@type="category_logo"
@id="category-logo-uploader"
class="no-repeat contain-image"
@@ -217,9 +175,9 @@ export default class UpsertCategoryAppearance extends Component {
@subtitle={{i18n "category.logo_description"}}
>
<UppyImageUploader
@imageUrl={{this.logoImageDarkUrl}}
@onUploadDone={{this.logoDarkUploadDone}}
@onUploadDeleted={{this.logoDarkUploadDeleted}}
@imageUrl={{this.logoDarkImageUrl}}
@onUploadDone={{fn this.onUploadDone "uploaded_logo_dark"}}
@onUploadDeleted={{fn this.onUploadDeleted "uploaded_logo_dark"}}
@type="category_logo_dark"
@id="category-dark-logo-uploader"
class="no-repeat contain-image"
@@ -229,8 +187,8 @@ export default class UpsertCategoryAppearance extends Component {
<@form.Container @title={{i18n "category.background_image"}}>
<UppyImageUploader
@imageUrl={{this.backgroundImageUrl}}
@onUploadDone={{this.backgroundUploadDone}}
@onUploadDeleted={{this.backgroundUploadDeleted}}
@onUploadDone={{fn this.onUploadDone "uploaded_background"}}
@onUploadDeleted={{fn this.onUploadDeleted "uploaded_background"}}
@type="category_background"
@id="category-background-uploader"
/>
@@ -239,24 +197,24 @@ export default class UpsertCategoryAppearance extends Component {
<@form.Container @title={{i18n "category.background_image_dark"}}>
<UppyImageUploader
@imageUrl={{this.backgroundDarkImageUrl}}
@onUploadDone={{this.backgroundDarkUploadDone}}
@onUploadDeleted={{this.backgroundDarkUploadDeleted}}
@onUploadDone={{fn this.onUploadDone "uploaded_background_dark"}}
@onUploadDeleted={{fn
this.onUploadDeleted
"uploaded_background_dark"
}}
@type="category_background_dark"
@id="category-dark-background-uploader"
/>
</@form.Container>
{{! This field is removed from edit-category-general when the UC is active }}
{{#if this.siteSettings.enable_simplified_category_creation}}
<@form.Field
@name="text_color"
@title={{i18n "category.foreground_color"}}
@format="large"
as |field|
>
<field.Color @colors={{CATEGORY_TEXT_COLORS}} />
</@form.Field>
{{/if}}
<@form.Field
@name="text_color"
@title={{i18n "category.foreground_color"}}
@format="large"
as |field|
>
<field.Color @colors={{CATEGORY_TEXT_COLORS}} />
</@form.Field>
<@form.Field
@name="default_view"
@@ -266,12 +224,12 @@ export default class UpsertCategoryAppearance extends Component {
>
<field.Custom>
<ComboBox
@valueProperty="value"
@id="category-default-view"
@content={{this.availableViews}}
@value={{field.value}}
@valueProperty="value"
@onChange={{field.set}}
@options={{hash placementStrategy="absolute"}}
@options={{hash none="category.sort_options.default"}}
/>
</field.Custom>
</@form.Field>
@@ -284,12 +242,12 @@ export default class UpsertCategoryAppearance extends Component {
>
<field.Custom>
<ComboBox
@valueProperty="value"
@id="category-default-period"
@content={{this.availableTopPeriods}}
@id="category-default-top-period"
@content={{this.topPeriods}}
@value={{field.value}}
@valueProperty="value"
@onChange={{field.set}}
@options={{hash placementStrategy="absolute"}}
@options={{hash none="category.sort_options.default"}}
/>
</field.Custom>
</@form.Field>
@@ -302,22 +260,22 @@ export default class UpsertCategoryAppearance extends Component {
>
<field.Custom>
<ComboBox
@valueProperty="value"
@content={{this.availableSorts}}
@id="category-sort-order"
@content={{this.sortOrders}}
@value={{field.value}}
@options={{hash none="category.sort_options.default"}}
@valueProperty="value"
@onChange={{field.set}}
@options={{hash none="category.sort_options.default"}}
/>
{{#unless this.isDefaultSortOrder}}
<ComboBox
@valueProperty="value"
@id="category-sort-ascending"
@content={{this.sortAscendingOptions}}
@value={{this.sortAscendingOption}}
@options={{hash
none="category.sort_options.default"
placementStrategy="absolute"
}}
@valueProperty="value"
@onChange={{this.onSortAscendingChange}}
@options={{hash none="category.sort_options.default"}}
/>
{{/unless}}
</field.Custom>
@@ -331,11 +289,12 @@ export default class UpsertCategoryAppearance extends Component {
>
<field.Custom>
<ComboBox
@id="category-default-filter"
@valueProperty="value"
@content={{this.availableListFilters}}
@id="category-default-list-filter"
@content={{this.listFilters}}
@value={{field.value}}
@valueProperty="value"
@onChange={{field.set}}
@options={{hash none="category.sort_options.default"}}
/>
</field.Custom>
</@form.Field>
@@ -359,12 +318,11 @@ export default class UpsertCategoryAppearance extends Component {
>
<field.Custom>
<ComboBox
@valueProperty="value"
@id="subcategory-list-style"
@content={{this.availableSubcategoryListStyles}}
@content={{this.subcategoryListStyles}}
@value={{field.value}}
@valueProperty="value"
@onChange={{field.set}}
@options={{hash placementStrategy="absolute"}}
/>
</field.Custom>
</@form.Field>
@@ -49,7 +49,7 @@ export default class UpsertCategorySecurity extends Component {
: parent.group_permissions;
}
get parentIsRestricted() {
get isParentRestricted() {
const parentPerms = this.parentPermissions;
if (!parentPerms?.length) {
return false;
@@ -64,7 +64,7 @@ export default class UpsertCategorySecurity extends Component {
let groups = this.site.groups.filter((g) => !permissionGroupIds.has(g.id));
if (this.parentIsRestricted) {
if (this.isParentRestricted) {
const parentGroupIds = new Set(
this.parentPermissions.map((p) => p.group_id)
);
@@ -79,7 +79,7 @@ export default class UpsertCategorySecurity extends Component {
}
get allParentGroupsUsed() {
return this.parentIsRestricted && !this.hasAvailableGroups;
return this.isParentRestricted && !this.hasAvailableGroups;
}
get everyonePermission() {
@@ -11,7 +11,6 @@ import lazyHash from "discourse/helpers/lazy-hash";
import withEventValue from "discourse/helpers/with-event-value";
import { SEARCH_PRIORITIES } from "discourse/lib/constants";
import getUrl from "discourse/lib/get-url";
import { applyMutableValueTransformer } from "discourse/lib/transformer";
import ComboBox from "discourse/select-kit/components/combo-box";
import GroupChooser from "discourse/select-kit/components/group-chooser";
import { eq } from "discourse/truth-helpers";
@@ -29,68 +28,6 @@ export default class UpsertCategorySettings extends Component {
return this.siteSettings.fixed_category_positions;
}
get isParentCategory() {
const parentCategoryId =
this.args.transientData?.parent_category_id ??
this.args.category.parent_category_id;
return this.args.category.isParent || !parentCategoryId;
}
get availableSubcategoryListStyles() {
return [
{ name: i18n("category.subcategory_list_styles.rows"), value: "rows" },
{
name: i18n(
"category.subcategory_list_styles.rows_with_featured_topics"
),
value: "rows_with_featured_topics",
},
{
name: i18n("category.subcategory_list_styles.boxes"),
value: "boxes",
},
{
name: i18n(
"category.subcategory_list_styles.boxes_with_featured_topics"
),
value: "boxes_with_featured_topics",
},
];
}
get availableViews() {
const views = [
{ name: i18n("filters.hot.title"), value: "hot" },
{ name: i18n("filters.latest.title"), value: "latest" },
{ name: i18n("filters.top.title"), value: "top" },
];
const context = {
categoryId: this.args.category.id,
customFields: this.args.category.custom_fields,
};
return applyMutableValueTransformer(
"category-available-views",
views,
context
);
}
get availableTopPeriods() {
return ["all", "yearly", "quarterly", "monthly", "weekly", "daily"].map(
(p) => {
return { name: i18n(`filters.top.${p}.title`), value: p };
}
);
}
get availableListFilters() {
return ["all", "none"].map((p) => {
return { name: i18n(`category.list_filters.${p}`), value: p };
});
}
get searchPrioritiesOptions() {
const options = [];
@@ -106,39 +43,6 @@ export default class UpsertCategorySettings extends Component {
return options;
}
get availableSorts() {
return applyMutableValueTransformer("category-sort-orders", [
"likes",
"op_likes",
"views",
"posts",
"activity",
"posters",
"category",
"created",
])
.map((s) => ({ name: i18n("category.sort_options." + s), value: s }))
.toSorted((a, b) => a.name.localeCompare(b.name));
}
get sortAscendingOption() {
const sortAscending = this.args.transientData?.sort_ascending;
if (sortAscending === "false") {
return false;
}
if (sortAscending === "true") {
return true;
}
return sortAscending;
}
get sortAscendingOptions() {
return [
{ name: i18n("category.sort_ascending"), value: true },
{ name: i18n("category.sort_descending"), value: false },
];
}
get hiddenRelativeIntervals() {
return ["mins"];
}
@@ -237,20 +141,17 @@ export default class UpsertCategorySettings extends Component {
(if (eq @selectedTab "settings") "active")
}}
>
{{! This field is removed from edit-category-general when the UC is active }}
{{#if this.siteSettings.enable_simplified_category_creation}}
<@form.Field
@name="slug"
@title={{i18n "category.slug"}}
@format="large"
as |field|
>
<field.Input
placeholder={{i18n "category.slug_placeholder"}}
@maxlength="255"
/>
</@form.Field>
{{/if}}
<@form.Field
@name="slug"
@title={{i18n "category.slug"}}
@format="large"
as |field|
>
<field.Input
placeholder={{i18n "category.slug_placeholder"}}
@maxlength="255"
/>
</@form.Field>
{{#if this.showPositionInput}}
<@form.Field
@@ -454,7 +355,7 @@ export default class UpsertCategorySettings extends Component {
<PluginOutlet
@name="category-email-in"
@connectorTagName="div"
@outletArgs={{lazyHash category=@category}}
@outletArgs={{lazyHash category=@category form=@form}}
/>
{{else}}
<@form.Alert @type="info">