mirror of
https://github.com/discourse/discourse.git
synced 2026-08-02 09:29:37 -05:00
DEV: Replace deprecated Ember's array uniq and uniqBy (#35227)
This commit replaces several instances of `.uniq`, `.uniqBy`, and related array deduplication methods with a new utility function `uniqueItemsFromArray`. This change ensures proper deduplication logic across the codebase while addressing the deprecation issues. **Main Changes:** * Created new utility function `uniqueItemsFromArray`: Located in a new file, `array-tools.js`, this function provides a reusable and configurable alternative for deduplication. * Replaced old methods: Updated multiple files to use the new utility function instead of deprecated or custom implementations for deduplication. * Replaced manual deduplication: Replace uses of `[...new Set(array)]` to standardize the deduplication logic across the codebase. * Added unit tests: Introduced thorough test coverage (`array-tools-test.js`) to validate functionality and edge cases for the utility function. * Updated deprecation workflow: Added logging for deprecated uniq and uniqBy methods to the `deprecation-workflow.js`. This change is primarily focused on code quality improvements, ensuring future-proof deduplication, and maintaining alignment with deprecation guidelines.
This commit is contained in:
@@ -15,6 +15,7 @@ import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import number from "discourse/helpers/number";
|
||||
import { reportModeComponent } from "discourse/lib/admin-report-additional-modes";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { isTesting } from "discourse/lib/environment";
|
||||
import { exportEntity } from "discourse/lib/export-csv";
|
||||
@@ -381,7 +382,7 @@ export default class AdminReport extends Component {
|
||||
// T and T+x, and all the ajax responses would occur after T+(x+y)
|
||||
// to avoid any inconsistencies we filter by period and make sure
|
||||
// the array contains only unique values
|
||||
let filteredReports = this._reports.uniqBy("report_key");
|
||||
let filteredReports = uniqueItemsFromArray(this._reports, "report_key");
|
||||
let foundReport;
|
||||
|
||||
const sort = (report) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import withEventValue from "discourse/helpers/with-event-value";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import UppyUpload from "discourse/lib/uppy/uppy-upload";
|
||||
import { i18n } from "discourse-i18n";
|
||||
@@ -66,7 +67,7 @@ export default class EmojiUploader extends Component {
|
||||
createEmojiGroup(group) {
|
||||
let newEmojiGroups = this.newEmojiGroups;
|
||||
if (group !== DEFAULT_GROUP) {
|
||||
newEmojiGroups = this.emojiGroups.concat([group]).uniq();
|
||||
newEmojiGroups = uniqueItemsFromArray(this.emojiGroups.concat([group]));
|
||||
}
|
||||
this.setProperties({
|
||||
newEmojiGroups,
|
||||
|
||||
@@ -14,6 +14,7 @@ import DButton from "discourse/components/d-button";
|
||||
import JsonSchemaEditorModal from "discourse/components/modal/json-schema-editor";
|
||||
import basePath from "discourse/helpers/base-path";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { deepEqual } from "discourse/lib/object";
|
||||
import { sanitize } from "discourse/lib/text";
|
||||
@@ -383,7 +384,9 @@ export default class SiteSettingComponent extends Component {
|
||||
setDefaultValues() {
|
||||
this.buffered.set(
|
||||
"value",
|
||||
this.bufferedValues.concat(this.defaultValues).uniq().join("|")
|
||||
uniqueItemsFromArray(this.bufferedValues.concat(this.defaultValues)).join(
|
||||
"|"
|
||||
)
|
||||
);
|
||||
this.setting.validationMessage = null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import ListSetting from "select-kit/components/list-setting";
|
||||
|
||||
@@ -17,13 +18,11 @@ export default class CompactList extends Component {
|
||||
}
|
||||
|
||||
get settingChoices() {
|
||||
return [
|
||||
...new Set([
|
||||
...makeArray(this.settingValue),
|
||||
...makeArray(this.args.setting.choices),
|
||||
...makeArray(this.createdChoices),
|
||||
]),
|
||||
];
|
||||
return uniqueItemsFromArray([
|
||||
...makeArray(this.settingValue),
|
||||
...makeArray(this.args.setting.choices),
|
||||
...makeArray(this.createdChoices),
|
||||
]);
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -33,9 +32,10 @@ export default class CompactList extends Component {
|
||||
|
||||
@action
|
||||
onChangeChoices(choices) {
|
||||
this.createdChoices = [
|
||||
...new Set([...makeArray(this.createdChoices), ...makeArray(choices)]),
|
||||
];
|
||||
this.createdChoices = uniqueItemsFromArray([
|
||||
...makeArray(this.createdChoices),
|
||||
...makeArray(choices),
|
||||
]);
|
||||
}
|
||||
|
||||
<template>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ListSetting from "select-kit/components/list-setting";
|
||||
@@ -39,13 +40,11 @@ export default class FileTypesList extends Component {
|
||||
}
|
||||
|
||||
get settingChoices() {
|
||||
return [
|
||||
...new Set([
|
||||
...makeArray(this.settingValue),
|
||||
...makeArray(this.args.setting.choices),
|
||||
...makeArray(this.createdChoices),
|
||||
]),
|
||||
];
|
||||
return uniqueItemsFromArray([
|
||||
...makeArray(this.settingValue),
|
||||
...makeArray(this.args.setting.choices),
|
||||
...makeArray(this.createdChoices),
|
||||
]);
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -55,9 +54,10 @@ export default class FileTypesList extends Component {
|
||||
|
||||
@action
|
||||
onChangeChoices(choices) {
|
||||
this.createdChoices = [
|
||||
...new Set([...makeArray(this.createdChoices), ...makeArray(choices)]),
|
||||
];
|
||||
this.createdChoices = uniqueItemsFromArray([
|
||||
...makeArray(this.createdChoices),
|
||||
...makeArray(choices),
|
||||
]);
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -79,7 +79,7 @@ export default class FileTypesList extends Component {
|
||||
}
|
||||
|
||||
const oldTypes = this.args.value.split(TOKEN_SEPARATOR);
|
||||
const newTypes = [...new Set([...oldTypes, ...types])];
|
||||
const newTypes = uniqueItemsFromArray([...oldTypes, ...types]);
|
||||
const diffTypes = newTypes.filter((type) => !oldTypes.includes(type));
|
||||
|
||||
if (isEmpty(diffTypes)) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { empty, reads } from "@ember/object/computed";
|
||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
@@ -113,7 +114,7 @@ export default class ValueList extends Component {
|
||||
this.collection.removeObject(value);
|
||||
|
||||
if (this.choices) {
|
||||
this.set("choices", this.choices.concat([value]).uniq());
|
||||
this.set("choices", uniqueItemsFromArray(this.choices.concat([value])));
|
||||
} else {
|
||||
this.set("choices", makeArray(value));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { tracked } from "@glimmer/tracking";
|
||||
import { computed } from "@ember/object";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { observes } from "@ember-decorators/object";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import Group from "discourse/models/group";
|
||||
import RestModel from "discourse/models/rest";
|
||||
@@ -19,7 +20,10 @@ class WebHookExtras {
|
||||
}
|
||||
|
||||
addCategories(categories) {
|
||||
this.categories = this.categories.concat(categories).uniqBy((c) => c.id);
|
||||
this.categories = uniqueItemsFromArray(
|
||||
this.categories.concat(categories),
|
||||
"id"
|
||||
);
|
||||
}
|
||||
|
||||
get categoriesById() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import EmberObject, { action } from "@ember/object";
|
||||
import Service, { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const ALL_FILTER = "all";
|
||||
@@ -35,7 +36,9 @@ export default class AdminEmojis extends Service {
|
||||
}
|
||||
|
||||
get emojiGroups() {
|
||||
return [DEFAULT_GROUP].concat(this.emojis.map((e) => e.group)).uniq();
|
||||
return uniqueItemsFromArray(
|
||||
[DEFAULT_GROUP].concat(this.emojis.map((e) => e.group))
|
||||
);
|
||||
}
|
||||
|
||||
get filteringGroups() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Service, { service } from "@ember/service";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import scrollLock from "discourse/lib/scroll-lock";
|
||||
import { ADMIN_PANEL, MAIN_PANEL } from "discourse/lib/sidebar/panels";
|
||||
import AdminSearchModal from "admin/components/modal/admin-search";
|
||||
@@ -17,13 +18,11 @@ export default class AdminSidebarStateManager extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
this.keywords[link_name].navigation = [
|
||||
...new Set(
|
||||
this.keywords[link_name].navigation.concat(
|
||||
keywords.map((keyword) => keyword.toLowerCase())
|
||||
)
|
||||
),
|
||||
];
|
||||
this.keywords[link_name].navigation = uniqueItemsFromArray(
|
||||
this.keywords[link_name].navigation.concat(
|
||||
keywords.map((keyword) => keyword.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
maybeForceAdminSidebar(opts = {}) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import categoryBadge from "discourse/helpers/category-badge";
|
||||
import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
||||
import lazyHash from "discourse/helpers/lazy-hash";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import {
|
||||
CATEGORY_STYLE_TYPES,
|
||||
CATEGORY_TEXT_COLORS,
|
||||
@@ -47,12 +48,13 @@ export default class EditCategoryGeneral extends Component {
|
||||
@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();
|
||||
return uniqueItemsFromArray(
|
||||
this.siteSettings.category_colors
|
||||
.split("|")
|
||||
.filter(Boolean)
|
||||
.map((i) => i.toUpperCase())
|
||||
.concat(categories.map((c) => c.color.toUpperCase()))
|
||||
);
|
||||
}
|
||||
|
||||
@cached
|
||||
|
||||
@@ -2,6 +2,7 @@ import { fn, hash } from "@ember/helper";
|
||||
import { service } from "@ember/service";
|
||||
import { eq } from "truth-helpers";
|
||||
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class EditCategoryLocalizations extends buildCategoryPanel(
|
||||
@@ -16,7 +17,7 @@ export default class EditCategoryLocalizations extends buildCategoryPanel(
|
||||
(obj) => obj.value
|
||||
);
|
||||
const committed = this.transientData.localizations.map((obj) => obj.locale);
|
||||
const allLocales = [...supported, ...committed].uniq();
|
||||
const allLocales = uniqueItemsFromArray([...supported, ...committed]);
|
||||
|
||||
return allLocales.map((value) => ({
|
||||
name: this.languageNameLookup.getLanguageName(value),
|
||||
|
||||
@@ -19,6 +19,7 @@ import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import withEventValue from "discourse/helpers/with-event-value";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import {
|
||||
disableBodyScroll,
|
||||
enableBodyScroll,
|
||||
@@ -90,9 +91,9 @@ export default class EmojiPicker extends Component {
|
||||
});
|
||||
|
||||
addVisibleSections(sections) {
|
||||
this.visibleSections = makeArray(this.visibleSections)
|
||||
.concat(makeArray(sections))
|
||||
.uniq();
|
||||
this.visibleSections = uniqueItemsFromArray(
|
||||
makeArray(this.visibleSections).concat(makeArray(sections))
|
||||
);
|
||||
}
|
||||
|
||||
get sections() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { not } from "truth-helpers";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class TagChooserField extends Component {
|
||||
@@ -111,10 +112,10 @@ export default class TagChooserField extends Component {
|
||||
set(
|
||||
this.composer.model,
|
||||
"tags",
|
||||
[
|
||||
uniqueItemsFromArray([
|
||||
...this.tags.filter((tag) => !this.selectedTags.includes(tag)),
|
||||
...selectedTags,
|
||||
].uniq()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Component from "@glimmer/component";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import lazyHash from "discourse/helpers/lazy-hash";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
|
||||
export default class GroupInfo extends Component {
|
||||
<template>
|
||||
@@ -24,7 +25,9 @@ export default class GroupInfo extends Component {
|
||||
|
||||
get names() {
|
||||
const { full_name, display_name, name } = this.args.group;
|
||||
return [...new Set([full_name, display_name, name].filter(Boolean))];
|
||||
return uniqueItemsFromArray(
|
||||
[full_name, display_name, name].filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
get name() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import EmberObject, { action } from "@ember/object";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import Badge from "discourse/models/badge";
|
||||
import UserBadge from "discourse/models/user-badge";
|
||||
@@ -25,7 +26,7 @@ export default class ShowController extends Controller {
|
||||
id: 0,
|
||||
badge: Badge.create({ name: i18n("badges.none") }),
|
||||
}),
|
||||
...filteredList.uniqBy("badge.name"),
|
||||
...uniqueItemsFromArray(filteredList, "badge.name"),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Controller from "@ember/controller";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { and } from "@ember/object/computed";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import { i18n } from "discourse-i18n";
|
||||
@@ -34,7 +35,7 @@ export default class UsersController extends Controller {
|
||||
usernames = usernames.split(",").filter(Boolean);
|
||||
}
|
||||
|
||||
return makeArray(usernames).uniq();
|
||||
return uniqueItemsFromArray(makeArray(usernames));
|
||||
}
|
||||
|
||||
@computed("model.allowed_pm_usernames")
|
||||
@@ -45,17 +46,23 @@ export default class UsersController extends Controller {
|
||||
usernames = usernames.split(",").filter(Boolean);
|
||||
}
|
||||
|
||||
return makeArray(usernames).uniq();
|
||||
return uniqueItemsFromArray(makeArray(usernames));
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeMutedUsernames(usernames) {
|
||||
this.model.set("muted_usernames", usernames.uniq().join(","));
|
||||
this.model.set(
|
||||
"muted_usernames",
|
||||
uniqueItemsFromArray(usernames).join(",")
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeAllowedPmUsernames(usernames) {
|
||||
this.model.set("allowed_pm_usernames", usernames.uniq().join(","));
|
||||
this.model.set(
|
||||
"allowed_pm_usernames",
|
||||
uniqueItemsFromArray(usernames).join(",")
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed("model.user_option.allow_private_messages")
|
||||
|
||||
@@ -23,6 +23,7 @@ import { MIN_POSTS_COUNT } from "discourse/components/topic-map/topic-map-summar
|
||||
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { BookmarkFormData } from "discourse/lib/bookmark-form-data";
|
||||
import { resetCachedTopicList } from "discourse/lib/cached-topic-list";
|
||||
import discourseComputed, { bind } from "discourse/lib/decorators";
|
||||
@@ -1071,9 +1072,11 @@ export default class TopicController extends Controller {
|
||||
selectReplies(post) {
|
||||
ajax(`/posts/${post.id}/reply-ids.json`).then((replies) => {
|
||||
const replyIds = replies.map((r) => r.id);
|
||||
this.selectedPostIds = [
|
||||
...new Set([...this.selectedPostIds, post.id, ...replyIds]),
|
||||
];
|
||||
this.selectedPostIds = uniqueItemsFromArray([
|
||||
...this.selectedPostIds,
|
||||
post.id,
|
||||
...replyIds,
|
||||
]);
|
||||
this._forceRefreshPostStream();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -313,6 +313,14 @@ const DeprecationWorkflow = new DiscourseDeprecationWorkflow([
|
||||
handler: "log",
|
||||
matchId: "discourse.native-array-extensions.toArray",
|
||||
},
|
||||
{
|
||||
handler: "log",
|
||||
matchId: "discourse.native-array-extensions.uniq",
|
||||
},
|
||||
{
|
||||
handler: "log",
|
||||
matchId: "discourse.native-array-extensions.uniqBy",
|
||||
},
|
||||
{
|
||||
handler: "log",
|
||||
matchId: "discourse.native-array-extensions.without",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { get } from "@ember/object";
|
||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||
|
||||
/**
|
||||
* Removes all occurrences of a specified value from an array.
|
||||
*
|
||||
@@ -30,3 +33,74 @@ export function removeValuesFromArray(array, values) {
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the selector argument into a callable function.
|
||||
* @param {(string|number|Function)} selector
|
||||
* @returns {Function}
|
||||
* @throws {Error} when selector type is invalid
|
||||
*/
|
||||
function buildSelector(selector) {
|
||||
switch (typeof selector) {
|
||||
case "number":
|
||||
return (item) => item[selector];
|
||||
case "string":
|
||||
return selector.includes(".")
|
||||
? (item) => get(item, selector)
|
||||
: (item) => item[selector];
|
||||
case "function":
|
||||
return selector;
|
||||
default:
|
||||
throw new Error(
|
||||
"uniqueItemsFromArray: the `selector` argument must be a string/number key " +
|
||||
`or a function, got ${typeof selector} instead`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array with unique items based on the provided selector function.
|
||||
* @param {Array} array
|
||||
* @param {Function} selectorFn
|
||||
* @returns {Array}
|
||||
*/
|
||||
function dedupeBy(array, selectorFn) {
|
||||
const uniqueKeys = new Set();
|
||||
const result = [];
|
||||
for (const item of array) {
|
||||
const key = selectorFn(item);
|
||||
if (!uniqueKeys.has(key)) {
|
||||
uniqueKeys.add(key);
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array containing the unique elements from the input array.
|
||||
* Elements are determined to be unique based on either strict equality or a specified condition.
|
||||
*
|
||||
* @param {Array} array - The input array from which unique elements are to be extracted.
|
||||
* Must be a valid array. An error is thrown if the input is not an array.
|
||||
* @param {(string|number|Function)} [selector] - Optional selector to determine uniqueness:
|
||||
* - If undefined: uses strict equality comparison
|
||||
* - If string/number: uses the value at the specified object property path
|
||||
* - If function: uses the return value of the function called with each item
|
||||
* @throws {Error} If array is not an Array or if selector is an invalid type
|
||||
* @return {Array|TrackedArray} A new array with only unique elements (TrackedArray if input was TrackedArray)
|
||||
*/
|
||||
export function uniqueItemsFromArray(array, selector) {
|
||||
if (!Array.isArray(array)) {
|
||||
throw new Error(
|
||||
`uniqueItemsFromArray expects an array as first argument, got ${typeof array} instead`
|
||||
);
|
||||
}
|
||||
|
||||
const items =
|
||||
selector === undefined
|
||||
? [...new Set(array)]
|
||||
: dedupeBy(array, buildSelector(selector));
|
||||
|
||||
return array instanceof TrackedArray ? new TrackedArray(items) : items;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { action } from "@ember/object";
|
||||
import { getOwner, setOwner } from "@ember/owner";
|
||||
import { service } from "@ember/service";
|
||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
import Topic from "discourse/models/topic";
|
||||
|
||||
@@ -33,7 +34,7 @@ export default class BulkSelectHelper {
|
||||
}
|
||||
|
||||
get selectedCategoryIds() {
|
||||
return this.selected.map((item) => item.category_id).uniq();
|
||||
return uniqueItemsFromArray(this.selected.map((item) => item.category_id));
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import domFromString from "discourse/lib/dom-from-string";
|
||||
import { getHashtagTypeClasses } from "discourse/lib/hashtag-type-registry";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
@@ -48,10 +49,9 @@ export function linkSeenHashtagsInContext(
|
||||
);
|
||||
});
|
||||
|
||||
return slugs
|
||||
.map((slug) => slug.toLowerCase())
|
||||
.uniq()
|
||||
.filter((slug) => !checkedHashtags.has(slug));
|
||||
return uniqueItemsFromArray(slugs.map((slug) => slug.toLowerCase())).filter(
|
||||
(slug) => !checkedHashtags.has(slug)
|
||||
);
|
||||
}
|
||||
|
||||
function _findAndReplaceSeenHashtagPlaceholder(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { formatUsername } from "discourse/lib/utilities";
|
||||
@@ -80,12 +81,9 @@ export function linkSeenMentions(element, siteSettings) {
|
||||
const names = mentions.map((mention) => mention.innerText.slice(1));
|
||||
updateFound(mentions, names);
|
||||
|
||||
return names
|
||||
.uniq()
|
||||
.filter(
|
||||
(name) =>
|
||||
!checked[name] && name.length >= siteSettings.min_username_length
|
||||
);
|
||||
return uniqueItemsFromArray(names).filter(
|
||||
(name) => !checked[name] && name.length >= siteSettings.min_username_length
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchUnseenMentions({ names, topicId, allowedNames }) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Used for Category.asyncFindByIds
|
||||
//
|
||||
// It's a cache that handles multiple lookups at a time.
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
|
||||
export class MultiCache {
|
||||
constructor(cb) {
|
||||
this.cb = cb;
|
||||
@@ -22,7 +24,7 @@ export class MultiCache {
|
||||
this.fetchTimes = [this.fetchTimes[this.fetchTimes.length - 1], new Date()];
|
||||
|
||||
const notFound = [];
|
||||
ids = ids.uniq();
|
||||
ids = uniqueItemsFromArray(ids);
|
||||
|
||||
for (const id of ids) {
|
||||
if (!this.values.has(id)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { dasherize, underscore } from "@ember/string";
|
||||
import { Promise } from "rsvp";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import I18n, { i18n } from "discourse-i18n";
|
||||
@@ -50,8 +51,10 @@ export default class Reviewable extends RestModel {
|
||||
|
||||
@discourseComputed("humanNoun")
|
||||
flaggedReviewableContextQuestion(humanNoun) {
|
||||
const uniqueReviewableScores =
|
||||
this.reviewable_scores.uniqBy("score_type.type");
|
||||
const uniqueReviewableScores = uniqueItemsFromArray(
|
||||
this.reviewable_scores,
|
||||
"score_type.type"
|
||||
);
|
||||
|
||||
if (uniqueReviewableScores.length === 1) {
|
||||
if (uniqueReviewableScores[0].score_type.type === "notify_moderators") {
|
||||
@@ -62,9 +65,11 @@ export default class Reviewable extends RestModel {
|
||||
}
|
||||
|
||||
const listOfQuestions = I18n.listJoiner(
|
||||
uniqueReviewableScores
|
||||
.map((score) => score.score_type.title.toLowerCase())
|
||||
.uniq(),
|
||||
uniqueItemsFromArray(
|
||||
uniqueReviewableScores.map((score) =>
|
||||
score.score_type.title.toLowerCase()
|
||||
)
|
||||
),
|
||||
i18n("review.context_question.delimiter")
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { htmlSafe } from "@ember/template";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { Promise } from "rsvp";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { url } from "discourse/lib/computed";
|
||||
import {
|
||||
INTERFACE_COLOR_MODES,
|
||||
@@ -1160,8 +1161,7 @@ export default class User extends RestModel.extend(Evented) {
|
||||
}
|
||||
});
|
||||
|
||||
return titles
|
||||
.uniq()
|
||||
return uniqueItemsFromArray(titles)
|
||||
.sort()
|
||||
.map((title) => {
|
||||
return {
|
||||
@@ -1240,7 +1240,7 @@ export default class User extends RestModel.extend(Evented) {
|
||||
calculateMutedIds(notificationLevel, id, type) {
|
||||
const muted_ids = this.get(type);
|
||||
if (notificationLevel === NotificationLevels.MUTED) {
|
||||
return muted_ids.concat(id).uniq();
|
||||
return uniqueItemsFromArray(muted_ids.concat(id));
|
||||
} else {
|
||||
return muted_ids.filter((existing_id) => existing_id !== id);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
|
||||
export default class MentionsParser {
|
||||
constructor(engine) {
|
||||
this.engine = engine;
|
||||
@@ -6,7 +8,7 @@ export default class MentionsParser {
|
||||
parse(markdown) {
|
||||
const tokens = this.engine.parse(markdown);
|
||||
const mentions = this.#parse(tokens);
|
||||
return [...new Set(mentions)];
|
||||
return uniqueItemsFromArray(mentions);
|
||||
}
|
||||
|
||||
#parse(tokens) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mentionRegex } from "pretty-text/mentions";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { isBoundary } from "discourse/static/prosemirror/lib/markdown-it";
|
||||
import { i18n } from "discourse-i18n";
|
||||
@@ -159,7 +160,7 @@ const extension = {
|
||||
|
||||
async function fetchMentions(names, context) {
|
||||
// only fetch new mentions that are not already validated
|
||||
names = names.uniq().filter((name) => {
|
||||
names = uniqueItemsFromArray(names).filter((name) => {
|
||||
return !VALID_MENTIONS.has(name) && !INVALID_MENTIONS.has(name);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { h } from "virtual-dom";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { iconNode } from "discourse/lib/icon-library";
|
||||
import { replaceEmoji } from "discourse/widgets/emoji";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
@@ -41,7 +42,10 @@ export default createWidget("post-links", {
|
||||
}
|
||||
|
||||
// only show incoming
|
||||
const links = this.attrs.links.filter((l) => l.reflection).uniqBy("title");
|
||||
const links = uniqueItemsFromArray(
|
||||
this.attrs.links.filter((l) => l.reflection),
|
||||
"title"
|
||||
);
|
||||
|
||||
if (links.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { module, test } from "qunit";
|
||||
import {
|
||||
removeValueFromArray,
|
||||
removeValuesFromArray,
|
||||
uniqueItemsFromArray,
|
||||
} from "discourse/lib/array-tools";
|
||||
|
||||
module("Unit | Lib | array-tools", function () {
|
||||
@@ -176,4 +177,206 @@ module("Unit | Lib | array-tools", function () {
|
||||
assert.deepEqual(twice, [3], "second call is a no-op");
|
||||
});
|
||||
});
|
||||
|
||||
module("uniqueItemsFromArray()", function () {
|
||||
test("throws when input is not an array", function (assert) {
|
||||
assert.throws(
|
||||
() => uniqueItemsFromArray(null),
|
||||
/expects an array/,
|
||||
"null is rejected"
|
||||
);
|
||||
assert.throws(
|
||||
() => uniqueItemsFromArray(undefined),
|
||||
/expects an array/,
|
||||
"undefined is rejected"
|
||||
);
|
||||
assert.throws(
|
||||
() => uniqueItemsFromArray("not array"),
|
||||
/expects an array/,
|
||||
"string is rejected"
|
||||
);
|
||||
assert.throws(
|
||||
() => uniqueItemsFromArray({}),
|
||||
/expects an array/,
|
||||
"object is rejected"
|
||||
);
|
||||
assert.throws(
|
||||
() => uniqueItemsFromArray(123),
|
||||
/expects an array/,
|
||||
"number is rejected"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns a new empty array for empty input", function (assert) {
|
||||
const input = [];
|
||||
const result = uniqueItemsFromArray(input);
|
||||
|
||||
assert.notStrictEqual(
|
||||
result,
|
||||
input,
|
||||
"returns a new array instance even for empty input"
|
||||
);
|
||||
assert.deepEqual(result, [], "result is empty");
|
||||
});
|
||||
|
||||
test("deduplicates primitives by strict equality", function (assert) {
|
||||
const input = [1, 1, 2, 3, 2, 3, 4, 4, 4];
|
||||
const result = uniqueItemsFromArray(input);
|
||||
|
||||
assert.deepEqual(result, [1, 2, 3, 4], "unique primitives retained");
|
||||
assert.notStrictEqual(
|
||||
result,
|
||||
input,
|
||||
"does not return original array reference"
|
||||
);
|
||||
});
|
||||
|
||||
test("retains first occurrence order", function (assert) {
|
||||
const input = ["b", "a", "b", "c", "a", "d"];
|
||||
const result = uniqueItemsFromArray(input);
|
||||
|
||||
assert.deepEqual(
|
||||
result,
|
||||
["b", "a", "c", "d"],
|
||||
"order corresponds to first occurrences"
|
||||
);
|
||||
});
|
||||
|
||||
test("uses identity for objects and symbols", function (assert) {
|
||||
const a1 = { id: 1 };
|
||||
const a2 = { id: 1 }; // different reference
|
||||
const b = { id: 2 };
|
||||
const s1 = Symbol("x");
|
||||
const s2 = Symbol("x");
|
||||
|
||||
const input = [a1, a1, a2, b, s1, s1, s2];
|
||||
const result = uniqueItemsFromArray(input);
|
||||
|
||||
assert.strictEqual(result[0], a1, "first a1 kept");
|
||||
assert.strictEqual(result[1], a2, "distinct object with same shape kept");
|
||||
assert.strictEqual(result[2], b, "b kept");
|
||||
assert.strictEqual(result[3], s1, "first symbol for x is kept");
|
||||
assert.strictEqual(result[4], s2, "second symbol for x is kept");
|
||||
|
||||
assert.strictEqual(
|
||||
result.length,
|
||||
5,
|
||||
"duplicates by reference removed only"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns TrackedArray when input is TrackedArray", function (assert) {
|
||||
const tracked = new TrackedArray([1, 2, 2, 3, 3, 3]);
|
||||
const result = uniqueItemsFromArray(tracked);
|
||||
|
||||
assert.true(result instanceof TrackedArray, "result is a TrackedArray");
|
||||
assert.deepEqual(Array.from(result), [1, 2, 3], "deduplicated correctly");
|
||||
assert.notStrictEqual(
|
||||
result,
|
||||
tracked,
|
||||
"returns a new TrackedArray instance"
|
||||
);
|
||||
});
|
||||
|
||||
test("does not mutate the input array", function (assert) {
|
||||
const input = [1, 1, 2];
|
||||
const snapshot = input.slice();
|
||||
|
||||
const result = uniqueItemsFromArray(input);
|
||||
|
||||
assert.deepEqual(input, snapshot, "input remains unchanged");
|
||||
assert.deepEqual(result, [1, 2], "result is deduplicated");
|
||||
});
|
||||
|
||||
test("handles heterogeneous arrays", function (assert) {
|
||||
const sym = Symbol("s");
|
||||
const input = [0, false, null, undefined, "", sym, 0, false, NaN, NaN];
|
||||
|
||||
const result = uniqueItemsFromArray(input);
|
||||
|
||||
// Set treats NaN as the same value, which is desired here
|
||||
assert.strictEqual(
|
||||
result.length,
|
||||
7,
|
||||
"expected number of unique elements"
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.slice(0, 6),
|
||||
[0, false, null, undefined, "", sym],
|
||||
"first occurrences retained"
|
||||
);
|
||||
assert.true(Number.isNaN(result[6]), "NaN retained once");
|
||||
});
|
||||
|
||||
test("treats NaN as a single unique value", function (assert) {
|
||||
const result = uniqueItemsFromArray([NaN, NaN, 1, NaN]);
|
||||
assert.strictEqual(result.length, 2, "NaN appears once among uniques");
|
||||
assert.true(Number.isNaN(result[0]), "NaN present");
|
||||
});
|
||||
|
||||
// selector-based behavior
|
||||
|
||||
test("selector: number index picks property by numeric key", function (assert) {
|
||||
const input = [
|
||||
["a", 1],
|
||||
["a", 2],
|
||||
["b", 3],
|
||||
["b", 4],
|
||||
];
|
||||
const result = uniqueItemsFromArray(input, 0);
|
||||
assert.deepEqual(
|
||||
result,
|
||||
[
|
||||
["a", 1],
|
||||
["b", 3],
|
||||
],
|
||||
"unique by index 0"
|
||||
);
|
||||
});
|
||||
|
||||
test("selector: string key picks shallow property", function (assert) {
|
||||
const input = [
|
||||
{ id: 1, name: "x" },
|
||||
{ id: 1, name: "x2" },
|
||||
{ id: 2, name: "y" },
|
||||
];
|
||||
const result = uniqueItemsFromArray(input, "id");
|
||||
assert.strictEqual(result.length, 2, "two unique ids");
|
||||
assert.deepEqual(
|
||||
result.map((o) => o.name),
|
||||
["x", "y"],
|
||||
"keeps first by id"
|
||||
);
|
||||
});
|
||||
|
||||
test("selector: dotted path uses Ember get for nested property access", function (assert) {
|
||||
const input = [
|
||||
{ user: { id: 10 } },
|
||||
{ user: { id: 10 } },
|
||||
{ user: { id: 11 } },
|
||||
];
|
||||
const result = uniqueItemsFromArray(input, "user.id");
|
||||
assert.strictEqual(result.length, 2, "two unique nested ids");
|
||||
assert.deepEqual(
|
||||
result.map((o) => o.user.id),
|
||||
[10, 11],
|
||||
"keeps first by nested id"
|
||||
);
|
||||
});
|
||||
|
||||
test("selector: function selector determines uniqueness", function (assert) {
|
||||
const input = [
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 1, name: "Alice2" },
|
||||
{ id: 2, name: "Bob" },
|
||||
];
|
||||
const result = uniqueItemsFromArray(input, (o) => o.id);
|
||||
assert.strictEqual(result.length, 2, "two unique ids");
|
||||
assert.deepEqual(
|
||||
result.map((o) => o.name),
|
||||
["Alice", "Bob"],
|
||||
"keeps first by function selector"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed } from "@ember/object";
|
||||
import { readOnly } from "@ember/object/computed";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import MultiSelectComponent from "select-kit/components/multi-select";
|
||||
import {
|
||||
@@ -41,7 +42,9 @@ export default class ListSetting extends MultiSelectComponent {
|
||||
|
||||
deselect(value) {
|
||||
this.onChangeChoices &&
|
||||
this.onChangeChoices([...new Set([value, ...makeArray(this.choices)])]);
|
||||
this.onChangeChoices(
|
||||
uniqueItemsFromArray([value, ...makeArray(this.choices)])
|
||||
);
|
||||
|
||||
super.deselect(...arguments);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isPresent } from "@ember/utils";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import { and, not } from "truth-helpers";
|
||||
import componentForCollection from "discourse/helpers/component-for-collection";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import SelectKitComponent, {
|
||||
@@ -131,7 +132,7 @@ export default class MultiSelect extends SelectKitComponent {
|
||||
);
|
||||
|
||||
this.selectKit.change(
|
||||
[...new Set(newValues)],
|
||||
uniqueItemsFromArray(newValues),
|
||||
newContent.length
|
||||
? newContent
|
||||
: makeArray(this.defaultItem(value, value))
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@ember-decorators/component";
|
||||
import { createPopper } from "@popperjs/core";
|
||||
import { Promise } from "rsvp";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import discourseDebounce from "discourse/lib/debounce";
|
||||
import { bind as bindDecorator } from "discourse/lib/decorators";
|
||||
import deprecated from "discourse/lib/deprecated";
|
||||
@@ -729,9 +730,9 @@ export default class SelectKit extends Component {
|
||||
content = this.selectKit.modifyContent(content).filter(Boolean);
|
||||
|
||||
if (this.selectKit.valueProperty) {
|
||||
content = content.uniqBy(this.selectKit.valueProperty);
|
||||
content = uniqueItemsFromArray(content, this.selectKit.valueProperty);
|
||||
} else {
|
||||
content = content.uniq();
|
||||
content = uniqueItemsFromArray(content);
|
||||
}
|
||||
|
||||
if (this.selectKit.options.limitMatches) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { action, computed } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { attributeBindings, classNames } from "@ember-decorators/component";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import MultiSelectComponent from "select-kit/components/multi-select";
|
||||
@@ -67,14 +68,14 @@ export default class TagChooser extends MultiSelectComponent {
|
||||
|
||||
@computed("tags.[]")
|
||||
get value() {
|
||||
return makeArray(this.tags).uniq();
|
||||
return uniqueItemsFromArray(makeArray(this.tags));
|
||||
}
|
||||
|
||||
@computed("tags.[]")
|
||||
get content() {
|
||||
return makeArray(this.tags)
|
||||
.uniq()
|
||||
.map((t) => this.defaultItem(t, t));
|
||||
return uniqueItemsFromArray(makeArray(this.tags)).map((t) =>
|
||||
this.defaultItem(t, t)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -112,10 +113,9 @@ export default class TagChooser extends MultiSelectComponent {
|
||||
};
|
||||
|
||||
if (selectedTags.length || this.blockedTags.length) {
|
||||
data.selected_tags = selectedTags
|
||||
.concat(this.blockedTags)
|
||||
.uniq()
|
||||
.slice(0, 100);
|
||||
data.selected_tags = uniqueItemsFromArray(
|
||||
selectedTags.concat(this.blockedTags)
|
||||
).slice(0, 100);
|
||||
}
|
||||
|
||||
if (!this.everyTag) {
|
||||
@@ -158,6 +158,6 @@ export default class TagChooser extends MultiSelectComponent {
|
||||
results = results.sort((a, b) => a.id > b.id);
|
||||
}
|
||||
|
||||
return results.uniqBy("id");
|
||||
return uniqueItemsFromArray(results, "id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { makeArray } from "discourse/lib/helpers";
|
||||
import MultiSelectComponent from "select-kit/components/multi-select";
|
||||
@@ -27,14 +28,14 @@ export default class TagGroupChooser extends MultiSelectComponent {
|
||||
|
||||
@computed("tagGroups.[]")
|
||||
get value() {
|
||||
return makeArray(this.tagGroups).uniq();
|
||||
return uniqueItemsFromArray(makeArray(this.tagGroups));
|
||||
}
|
||||
|
||||
@computed("tagGroups.[]")
|
||||
get content() {
|
||||
return makeArray(this.tagGroups)
|
||||
.uniq()
|
||||
.map((t) => this.defaultItem(t, t));
|
||||
return uniqueItemsFromArray(makeArray(this.tagGroups)).map((t) =>
|
||||
this.defaultItem(t, t)
|
||||
);
|
||||
}
|
||||
|
||||
validateCreate(filter, content) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { setOwner } from "@ember/owner";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
|
||||
export default class ChatMessagesManager {
|
||||
@tracked messages = [];
|
||||
@@ -27,10 +28,10 @@ export default class ChatMessagesManager {
|
||||
}
|
||||
|
||||
addMessages(messages = []) {
|
||||
this.messages = this.messages
|
||||
.concat(messages)
|
||||
.uniqBy("id")
|
||||
.sort((a, b) => a.createdAt - b.createdAt);
|
||||
this.messages = uniqueItemsFromArray(
|
||||
this.messages.concat(messages),
|
||||
"id"
|
||||
).sort((a, b) => a.createdAt - b.createdAt);
|
||||
}
|
||||
|
||||
findMessage(messageId) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { cancel, next } from "@ember/runloop";
|
||||
import Service, { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import deprecated from "discourse/lib/deprecated";
|
||||
import discourseLater from "discourse/lib/later";
|
||||
@@ -392,10 +393,11 @@ export default class Chat extends Service {
|
||||
}
|
||||
|
||||
upsertDmChannelForUser(channel, user) {
|
||||
const usernames = (channel.chatable.users || [])
|
||||
.map((item) => item.username)
|
||||
.concat(user.username)
|
||||
.uniq();
|
||||
const usernames = uniqueItemsFromArray(
|
||||
(channel.chatable.users || [])
|
||||
.map((item) => item.username)
|
||||
.concat(user.username)
|
||||
);
|
||||
|
||||
return this.upsertDmChannel({ usernames });
|
||||
}
|
||||
@@ -411,8 +413,12 @@ export default class Chat extends Service {
|
||||
return ajax("/chat/api/direct-message-channels.json", {
|
||||
method: "POST",
|
||||
data: {
|
||||
target_usernames: targets.usernames?.uniq(),
|
||||
target_groups: targets.groups?.uniq(),
|
||||
target_usernames: targets.usernames
|
||||
? uniqueItemsFromArray(targets.usernames)
|
||||
: null,
|
||||
target_groups: targets.groups
|
||||
? uniqueItemsFromArray(targets.groups)
|
||||
: null,
|
||||
upsert: opts.upsert,
|
||||
name: opts.name,
|
||||
},
|
||||
@@ -434,7 +440,7 @@ export default class Chat extends Service {
|
||||
// participant to fetch the channel for.
|
||||
getDmChannelForUsernames(usernames) {
|
||||
return ajax("/chat/direct_messages.json", {
|
||||
data: { usernames: usernames.uniq().join(",") },
|
||||
data: { usernames: uniqueItemsFromArray(usernames).join(",") },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -9,6 +9,7 @@ import DButton from "discourse/components/d-button";
|
||||
import categoryBadge from "discourse/helpers/category-badge";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
export default class AiSplitTopicSuggester extends Component {
|
||||
@@ -88,10 +89,10 @@ export default class AiSplitTopicSuggester extends Component {
|
||||
if (this.args.currentValue) {
|
||||
if (Array.isArray(this.args.currentValue)) {
|
||||
const updatedTags = [...this.args.currentValue, suggestion];
|
||||
this.args.updateAction([...new Set(updatedTags)]);
|
||||
this.args.updateAction(uniqueItemsFromArray(updatedTags));
|
||||
} else {
|
||||
const updatedTags = [this.args.currentValue, suggestion];
|
||||
this.args.updateAction([...new Set(updatedTags)]);
|
||||
this.args.updateAction(uniqueItemsFromArray(updatedTags));
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(suggestion)) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { renderIcon } from "discourse/lib/icon-library";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DateWithZoneHelper from "./date-with-zone-helper";
|
||||
@@ -141,7 +142,7 @@ export default class LocalDateBuilder {
|
||||
});
|
||||
});
|
||||
|
||||
return previewedTimezones.uniqBy("timezone");
|
||||
return uniqueItemsFromArray(previewedTimezones, "timezone");
|
||||
}
|
||||
|
||||
_isEqualZones(timezoneA, timezoneB) {
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import { service } from "@ember/service";
|
||||
import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||
import { and } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
|
||||
import CustomReaction from "../models/discourse-reactions-custom-reaction";
|
||||
@@ -26,7 +27,7 @@ export default class DiscourseReactionsCounter extends Component {
|
||||
}
|
||||
|
||||
reactionsChanged(data) {
|
||||
data.reactions.uniq().forEach((reaction) => {
|
||||
uniqueItemsFromArray(data.reactions).forEach((reaction) => {
|
||||
this.getUsers(reaction);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Component from "@ember/component";
|
||||
import { classNames, tagName } from "@ember-decorators/component";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { uniqueItemsFromArray } from "discourse/lib/array-tools";
|
||||
import { afterRender } from "discourse/lib/decorators";
|
||||
import { REPLACEMENTS } from "discourse/lib/icon-library";
|
||||
import discourseLater from "discourse/lib/later";
|
||||
@@ -22,7 +23,7 @@ export default class StyleguideIcons extends Component {
|
||||
if (symbols.length > 0) {
|
||||
let ids = Array.from(symbols).map((item) => item.id);
|
||||
ids.push(...Object.keys(REPLACEMENTS));
|
||||
this.set("iconIds", [...new Set(ids.sort())]);
|
||||
this.set("iconIds", uniqueItemsFromArray(ids).sort());
|
||||
} else {
|
||||
// Let's try again a short time later if there are no svgs loaded yet
|
||||
discourseLater(this, this.setIconIds, 1500);
|
||||
|
||||
Reference in New Issue
Block a user