DEV: Replace deprecated Ember's array sortBy with sort (#34998)

This Pull Request introduces changes that replace the use of .sortBy
with .sort combined with compare from @ember/utils. This update aims to
modernize and standardize sorting operations throughout the codebase.

**Main Changes:**

* Replaced .sortBy with .sort and compare in various components,
controllers, and services to improve sorting practices.
* Updated sorting logic to handle optional chaining (?.) for increased
robustness.
* Adjusted sorting logic, including reversing, in some cases for more
clarity and correctness.
* Added a new deprecation workflow entry to handle sortBy deprecation
logs (discourse.native-array-extensions.sortBy).
This commit is contained in:
Sérgio Saquetim
2025-09-26 13:38:26 -03:00
committed by GitHub
parent 49ed4be80b
commit 14fef6598c
16 changed files with 76 additions and 37 deletions
@@ -1,6 +1,7 @@
import Component from "@glimmer/component";
import { concat } from "@ember/helper";
import { action } from "@ember/object";
import { compare } from "@ember/utils";
import { eq } from "truth-helpers";
import ConditionalLoadingSection from "discourse/components/conditional-loading-section";
import DButton from "discourse/components/d-button";
@@ -23,7 +24,7 @@ export default class DashboardProblems extends Component {
}
get problems() {
return this.args.problems.sortBy("priority");
return this.args.problems.sort((a, b) => compare(a?.priority, b?.priority));
}
<template>
@@ -3,6 +3,7 @@ import { action } from "@ember/object";
import { alias, empty, sort } from "@ember/object/computed";
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import { compare } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse/lib/decorators";
import { grantableBadges } from "discourse/lib/grant-badge-utils";
@@ -70,7 +71,7 @@ export default class AdminUserBadgesController extends Controller {
}
});
return expanded.sortBy("granted_at").reverse();
return expanded.sort((a, b) => compare(b?.granted_at, a?.granted_at)); // sort descending
}
@action
@@ -2,6 +2,7 @@
import Component from "@ember/component";
import { concat } from "@ember/helper";
import { computed } from "@ember/object";
import { compare } from "@ember/utils";
import { attributeBindings } from "@ember-decorators/component";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
@@ -17,10 +18,14 @@ export default class AnonymousTopicFooterButtons extends Component {
@computed("allButtons.[]")
get buttons() {
return this.allButtons
.filterBy("anonymousOnly", true)
.sortBy("priority")
.reverse();
return (
this.allButtons
.filterBy("anonymousOnly", true)
.sort((a, b) => compare(a?.priority, b?.priority))
// Reversing the array is necessary because when priorities are not set,
// we want to show the most recently added item first
.reverse()
);
}
<template>
@@ -2,6 +2,7 @@
import Component from "@ember/component";
import { hash } from "@ember/helper";
import { filter } from "@ember/object/computed";
import { compare } from "@ember/utils";
import { classNameBindings, tagName } from "@ember-decorators/component";
// A breadcrumb including category drop downs
import PluginOutlet from "discourse/components/plugin-outlet";
@@ -87,7 +88,9 @@ export default class BreadCrumbs extends Component {
return parentCategories;
}
return parentCategories.sortBy("totalTopicCount").reverse();
return parentCategories.sort(
(a, b) => compare(b?.totalTopicCount, a?.totalTopicCount) // sort descending
);
}
@discourseComputed("category")
@@ -4,6 +4,7 @@ import { concat, fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { compare } from "@ember/utils";
import { eq, not } from "truth-helpers";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
@@ -32,7 +33,7 @@ export default class ReorderCategories extends Component {
@tracked highlightedCategoryId = null;
get sortedEntries() {
return this.entries.sortBy("position");
return this.entries.sort((a, b) => compare(a?.position, b?.position));
}
reorder(from) {
@@ -41,7 +42,9 @@ export default class ReorderCategories extends Component {
position: category.position,
}));
return this.createEntries([...from.sortBy("position")]);
return this.createEntries([
...from.sort((a, b) => compare(a?.position, b?.position)),
]);
}
/**
@@ -4,6 +4,7 @@ import { concat, hash } from "@ember/helper";
import { computed } from "@ember/object";
import { alias, or } from "@ember/object/computed";
import { getOwner } from "@ember/owner";
import { compare } from "@ember/utils";
import { attributeBindings } from "@ember-decorators/component";
import { eq, gt } from "truth-helpers";
import BookmarkMenu from "discourse/components/bookmark-menu";
@@ -50,12 +51,16 @@ export default class TopicFooterButtons extends Component {
@computed("inlineButtons.[]", "inlineDropdowns.[]")
get inlineActionables() {
return this.inlineButtons
.filterBy("dropdown", false)
.filterBy("anonymousOnly", false)
.concat(this.inlineDropdowns)
.sortBy("priority")
.reverse();
return (
this.inlineButtons
.filterBy("dropdown", false)
.filterBy("anonymousOnly", false)
.concat(this.inlineDropdowns)
.sort((a, b) => compare(a?.priority, b?.priority))
// Reversing the array is necessary because when priorities are not set,
// we want to show the most recently added item first
.reverse()
);
}
@computed("topic")
@@ -5,7 +5,7 @@ import { alias, and, gt, gte, not, or } from "@ember/object/computed";
import { LinkTo } from "@ember/routing";
import { dasherize } from "@ember/string";
import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils";
import { compare, isEmpty } from "@ember/utils";
import {
attributeBindings,
classNameBindings,
@@ -160,7 +160,7 @@ export default class UserCardContents extends CardContentsBase {
const userFields = this.get("user.user_fields");
return siteUserFields
.filterBy("show_on_user_card", true)
.sortBy("position")
.sort((a, b) => compare(a?.position, b?.position))
.map((field) => {
set(field, "dasherized_name", dasherize(field.get("name")));
const value = userFields ? userFields[field.get("id")] : null;
@@ -2,7 +2,7 @@ import Controller from "@ember/controller";
import EmberObject, { action } from "@ember/object";
import { readOnly } from "@ember/object/computed";
import { service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import { compare, isEmpty } from "@ember/utils";
import FeatureTopicOnProfileModal from "discourse/components/modal/feature-topic-on-profile";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
@@ -60,10 +60,12 @@ export default class ProfileController extends Controller {
siteUserFields = siteUserFields.filterBy("editable", true);
}
return siteUserFields.sortBy("position").map((field) => {
const value = this.model.user_fields?.[field.id.toString()];
return EmberObject.create({ field, value });
});
return siteUserFields
.sort((a, b) => compare(a?.position, b?.position))
.map((field) => {
const value = this.model.user_fields?.[field.id.toString()];
return EmberObject.create({ field, value });
});
}
@discourseComputed("currentUser.needs_required_fields_check")
@@ -3,7 +3,7 @@ import EmberObject, { action, computed, set } from "@ember/object";
import { and, equal, gt, not, or, readOnly } from "@ember/object/computed";
import { service } from "@ember/service";
import { dasherize } from "@ember/string";
import { isEmpty } from "@ember/utils";
import { compare, isEmpty } from "@ember/utils";
import CanCheckEmailsHelper from "discourse/lib/can-check-emails-helper";
import { setting } from "discourse/lib/computed";
import discourseComputed from "discourse/lib/decorators";
@@ -161,7 +161,7 @@ export default class UserController extends Controller {
const userFields = this.get("model.user_fields");
return siteUserFields
.filterBy("show_on_profile", true)
.sortBy("position")
.sort((a, b) => compare(a?.position, b?.position))
.map((field) => {
set(field, "dasherized_name", dasherize(field.get("name")));
const value = userFields
@@ -273,6 +273,10 @@ const DeprecationWorkflow = new DiscourseDeprecationWorkflow([
handler: "log",
matchId: "discourse.native-array-extensions.rejectBy",
},
{
handler: "log",
matchId: "discourse.native-array-extensions.sortBy",
},
{
handler: "log",
matchId: "discourse.native-array-extensions.without",
@@ -1,5 +1,5 @@
import { tracked } from "@glimmer/tracking";
import { isEmpty } from "@ember/utils";
import { compare, isEmpty } from "@ember/utils";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import { i18n } from "discourse-i18n";
@@ -95,13 +95,15 @@ export default class UserFieldsValidationHelper {
if (userFields) {
const getValidationVisible = () => this.validationVisible;
this.userFields = new TrackedArray(
userFields.sortBy("position").map((f) => {
return new TrackedUserField({
field: f,
getValidationVisible,
getAccountPassword: this.getAccountPassword,
});
})
userFields
.sort((a, b) => compare(a?.position, b?.position))
.map((f) => {
return new TrackedUserField({
field: f,
getValidationVisible,
getAccountPassword: this.getAccountPassword,
});
})
);
}
}
@@ -2,6 +2,7 @@ import { tracked } from "@glimmer/tracking";
import { warn } from "@ember/debug";
import { computed, get } from "@ember/object";
import { service } from "@ember/service";
import { compare } from "@ember/utils";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse/lib/decorators";
import { getOwnerWithFallback } from "discourse/lib/get-owner";
@@ -398,7 +399,7 @@ export default class Category extends RestModel {
}
}
return data.sortBy("read_restricted");
return data.sort((a, b) => compare(a?.read_restricted, b?.read_restricted));
}
static async asyncHierarchicalSearch(term, opts) {
@@ -1,4 +1,5 @@
import Service, { service } from "@ember/service";
import { compare } from "@ember/utils";
import { TrackedSet } from "@ember-compat/tracked-built-ins";
import discourseDebounce from "discourse/lib/debounce";
import { isTesting } from "discourse/lib/environment";
@@ -24,7 +25,9 @@ export default class UserTips extends Service {
}
const newId = tipsArray
.sortBy("priority")
.sort((a, b) => compare(a?.priority, b?.priority))
// Reversing the array is necessary because when priorities are not set,
// we want to show the most recently added tip first
.reverse()
.find((tip) => this.canSeeUserTip(tip.id))?.id;
@@ -4,6 +4,7 @@ import { fn } from "@ember/helper";
import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { service } from "@ember/service";
import { compare } from "@ember/utils";
import DButton from "discourse/components/d-button";
import replaceEmoji from "discourse/helpers/replace-emoji";
import { ajax } from "discourse/lib/ajax";
@@ -17,7 +18,11 @@ export default class AdminChatIncomingWebhooksList extends Component {
@tracked loading = false;
get sortedWebhooks() {
return this.args.webhooks?.sortBy("updated_at").reverse() || [];
return (
this.args.webhooks?.sort(
(a, b) => compare(b?.updated_at, a?.updated_at) // sort descending
) || []
);
}
@action
@@ -4,6 +4,7 @@ import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { compare } from "@ember/utils";
import { eq } from "truth-helpers";
import { i18n } from "discourse-i18n";
import roundTime from "../../lib/round-time";
@@ -53,7 +54,7 @@ export default class GroupTimezones extends Component {
});
groupedTimezones = groupedTimezones
.sortBy("offset")
.sort((a, b) => compare(a?.offset, b?.offset))
.filter((g) => g.members.length);
let newDayIndex;
@@ -1,6 +1,7 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { compare } from "@ember/utils";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse/lib/decorators";
@@ -16,7 +17,9 @@ export default class AdminPluginsShowDiscourseGamificationLeaderboardsIndexContr
@discourseComputed("model.leaderboards.@each.updatedAt")
sortedLeaderboards(leaderboards) {
return leaderboards?.sortBy("updatedAt").reverse() || [];
return (
leaderboards?.sort((a, b) => compare(b?.updatedAt, a?.updatedAt)) || []
);
}
@action