From c0d4072aab56568a7cbeb3374d18a2517b4e1b29 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Mon, 3 Aug 2026 09:48:56 -0700 Subject: [PATCH 1/4] fix(scripting): sort selected devices by name when added --- frontend/src/components/DeviceListHeaderCheckbox.tsx | 3 ++- frontend/src/helpers/selectionRange.ts | 8 ++++++++ frontend/src/hooks/useSelect.ts | 12 +++++++++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/DeviceListHeaderCheckbox.tsx b/frontend/src/components/DeviceListHeaderCheckbox.tsx index 574d7c4e5..bfb7eb9d1 100644 --- a/frontend/src/components/DeviceListHeaderCheckbox.tsx +++ b/frontend/src/components/DeviceListHeaderCheckbox.tsx @@ -2,6 +2,7 @@ import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { State, Dispatch } from '../store' import { Checkbox } from '@mui/material' +import { sortSelectedIds } from '../helpers/selectionRange' import { Icon } from './Icon' type Props = { select?: boolean; devices: IDevice[] } @@ -16,7 +17,7 @@ export const DeviceListHeaderCheckbox: React.FC = ({ select, devices }) = const onClick = event => { event.stopPropagation() if (indeterminate || selected.length === 0) { - dispatch.ui.set({ selected: devices.map(d => d.id) }) + dispatch.ui.set({ selected: sortSelectedIds(devices.map(d => d.id), devices) }) } else { dispatch.ui.set({ selected: [], selectionAnchor: undefined }) } diff --git a/frontend/src/helpers/selectionRange.ts b/frontend/src/helpers/selectionRange.ts index 373e3a388..49daf3a30 100644 --- a/frontend/src/helpers/selectionRange.ts +++ b/frontend/src/helpers/selectionRange.ts @@ -24,6 +24,14 @@ export function mergeSelectedIds(selected: string[], idsToAdd: string[]) { return [...new Set([...selected, ...idsToAdd])] } +// Keeps the selection in name order from the first click, so it never reorders downstream. +export function sortSelectedIds(selected: string[], devices: IDevice[]) { + const names = new Map(devices.map(device => [device.id, device.name])) + return [...selected].sort((a, b) => + (names.get(a) || a).localeCompare(names.get(b) || b, undefined, { numeric: true, sensitivity: 'base' }) + ) +} + export function removeSelectedIds(selected: string[], idsToRemove: string[]) { const remove = new Set(idsToRemove) return selected.filter(id => !remove.has(id)) diff --git a/frontend/src/hooks/useSelect.ts b/frontend/src/hooks/useSelect.ts index 0a396585e..d3542436e 100644 --- a/frontend/src/hooks/useSelect.ts +++ b/frontend/src/hooks/useSelect.ts @@ -1,7 +1,13 @@ import { useDispatch, useSelector, useStore } from 'react-redux' import { Dispatch, State } from '../store' import { selectVisibleDevices } from '../selectors/devices' -import { getInclusiveIdRange, getSelectableDeviceIds, mergeSelectedIds, removeSelectedIds } from '../helpers/selectionRange' +import { + getInclusiveIdRange, + getSelectableDeviceIds, + mergeSelectedIds, + removeSelectedIds, + sortSelectedIds, +} from '../helpers/selectionRange' type UseSelectParams = { deviceId: string @@ -25,7 +31,7 @@ export const useSelect = ({ deviceId, selectMode }: UseSelectParams) => { if (range.length) { const rangeSelected = isSelected ? removeSelectedIds(nextSelected, range) : mergeSelectedIds(nextSelected, range) - dispatch.ui.set({ selected: rangeSelected }) + dispatch.ui.set({ selected: sortSelectedIds(rangeSelected, visibleDevices) }) dispatch.ui.set({ selectionAnchor: deviceId }) return } @@ -37,7 +43,7 @@ export const useSelect = ({ deviceId, selectMode }: UseSelectParams) => { nextSelected.push(deviceId) } - dispatch.ui.set({ selected: nextSelected }) + dispatch.ui.set({ selected: sortSelectedIds(nextSelected, visibleDevices) }) dispatch.ui.set({ selectionAnchor: deviceId }) } From d7c4ba05cea129f91734ea5d3bea31ded50e5497 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Mon, 3 Aug 2026 10:16:12 -0700 Subject: [PATCH 2/4] refactor(selection): collapse duplicate select branches and hoist the collator --- frontend/src/helpers/selectionRange.ts | 15 +++++++++++---- frontend/src/hooks/useSelect.ts | 23 ++++------------------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/frontend/src/helpers/selectionRange.ts b/frontend/src/helpers/selectionRange.ts index 49daf3a30..b62d4a6d5 100644 --- a/frontend/src/helpers/selectionRange.ts +++ b/frontend/src/helpers/selectionRange.ts @@ -24,12 +24,19 @@ export function mergeSelectedIds(selected: string[], idsToAdd: string[]) { return [...new Set([...selected, ...idsToAdd])] } -// Keeps the selection in name order from the first click, so it never reorders downstream. +// Reused across comparisons — localeCompare with options builds a new collator on every call. +const nameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) + +// Devices are selected in click order, so re-sort by name on each change to keep the +// selection ordered. Ids with no loaded device sort last rather than interleaving by raw id. export function sortSelectedIds(selected: string[], devices: IDevice[]) { const names = new Map(devices.map(device => [device.id, device.name])) - return [...selected].sort((a, b) => - (names.get(a) || a).localeCompare(names.get(b) || b, undefined, { numeric: true, sensitivity: 'base' }) - ) + return [...selected].sort((a, b) => { + const nameA = names.get(a) + const nameB = names.get(b) + if (!nameA || !nameB) return nameA ? -1 : nameB ? 1 : 0 + return nameCollator.compare(nameA, nameB) + }) } export function removeSelectedIds(selected: string[], idsToRemove: string[]) { diff --git a/frontend/src/hooks/useSelect.ts b/frontend/src/hooks/useSelect.ts index d3542436e..91288c6f2 100644 --- a/frontend/src/hooks/useSelect.ts +++ b/frontend/src/hooks/useSelect.ts @@ -23,28 +23,13 @@ export const useSelect = ({ deviceId, selectMode }: UseSelectParams) => { const handleSelect = (shiftKey?: boolean) => { const state = store.getState() const selected = state.ui.selected - const selectionAnchor = state.ui.selectionAnchor const visibleDevices = selectVisibleDevices(state) - const nextSelected = [...selected] const selectableIds = getSelectableDeviceIds(visibleDevices) - const range = shiftKey ? getInclusiveIdRange(selectableIds, selectionAnchor, deviceId) : [] + const range = shiftKey ? getInclusiveIdRange(selectableIds, state.ui.selectionAnchor, deviceId) : [] + const ids = range.length ? range : [deviceId] + const nextSelected = isSelected ? removeSelectedIds(selected, ids) : mergeSelectedIds(selected, ids) - if (range.length) { - const rangeSelected = isSelected ? removeSelectedIds(nextSelected, range) : mergeSelectedIds(nextSelected, range) - dispatch.ui.set({ selected: sortSelectedIds(rangeSelected, visibleDevices) }) - dispatch.ui.set({ selectionAnchor: deviceId }) - return - } - - if (isSelected) { - const index = nextSelected.indexOf(deviceId) - nextSelected.splice(index, 1) - } else { - nextSelected.push(deviceId) - } - - dispatch.ui.set({ selected: sortSelectedIds(nextSelected, visibleDevices) }) - dispatch.ui.set({ selectionAnchor: deviceId }) + dispatch.ui.set({ selected: sortSelectedIds(nextSelected, visibleDevices), selectionAnchor: deviceId }) } return { isSelected, isAnchorRow, handleSelect } From 230d32074ab90056d1e4374bf12518a7ca37b2e5 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Mon, 3 Aug 2026 12:27:10 -0700 Subject: [PATCH 3/4] refactor(selection): order the selection by list position instead of by name --- .../src/components/DeviceListHeaderCheckbox.tsx | 3 +-- frontend/src/helpers/selectionRange.ts | 17 +++++------------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/DeviceListHeaderCheckbox.tsx b/frontend/src/components/DeviceListHeaderCheckbox.tsx index bfb7eb9d1..574d7c4e5 100644 --- a/frontend/src/components/DeviceListHeaderCheckbox.tsx +++ b/frontend/src/components/DeviceListHeaderCheckbox.tsx @@ -2,7 +2,6 @@ import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { State, Dispatch } from '../store' import { Checkbox } from '@mui/material' -import { sortSelectedIds } from '../helpers/selectionRange' import { Icon } from './Icon' type Props = { select?: boolean; devices: IDevice[] } @@ -17,7 +16,7 @@ export const DeviceListHeaderCheckbox: React.FC = ({ select, devices }) = const onClick = event => { event.stopPropagation() if (indeterminate || selected.length === 0) { - dispatch.ui.set({ selected: sortSelectedIds(devices.map(d => d.id), devices) }) + dispatch.ui.set({ selected: devices.map(d => d.id) }) } else { dispatch.ui.set({ selected: [], selectionAnchor: undefined }) } diff --git a/frontend/src/helpers/selectionRange.ts b/frontend/src/helpers/selectionRange.ts index b62d4a6d5..4d4e885c4 100644 --- a/frontend/src/helpers/selectionRange.ts +++ b/frontend/src/helpers/selectionRange.ts @@ -24,19 +24,12 @@ export function mergeSelectedIds(selected: string[], idsToAdd: string[]) { return [...new Set([...selected, ...idsToAdd])] } -// Reused across comparisons — localeCompare with options builds a new collator on every call. -const nameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) - -// Devices are selected in click order, so re-sort by name on each change to keep the -// selection ordered. Ids with no loaded device sort last rather than interleaving by raw id. +// Devices are selected in click order, so re-order on each change to follow the list the user +// is looking at — which respects whatever sort they've chosen. Ids no longer in the list (a +// selection outliving a filter change) keep their relative order at the end. export function sortSelectedIds(selected: string[], devices: IDevice[]) { - const names = new Map(devices.map(device => [device.id, device.name])) - return [...selected].sort((a, b) => { - const nameA = names.get(a) - const nameB = names.get(b) - if (!nameA || !nameB) return nameA ? -1 : nameB ? 1 : 0 - return nameCollator.compare(nameA, nameB) - }) + const order = new Map(devices.map((device, index) => [device.id, index])) + return [...selected].sort((a, b) => (order.get(a) ?? Infinity) - (order.get(b) ?? Infinity)) } export function removeSelectedIds(selected: string[], idsToRemove: string[]) { From abe200fc69408174a2f13f787cbde9bc824c5c6b Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Mon, 3 Aug 2026 12:26:29 -0700 Subject: [PATCH 4/4] refactor(sort): route alphabetical sorts through a shared comparator --- frontend/src/components/FilterDrawer.tsx | 3 ++- frontend/src/components/OrganizationGuestList.tsx | 8 ++------ frontend/src/components/OrganizationMemberList.tsx | 8 ++------ frontend/src/components/OrganizationSelect.tsx | 3 ++- frontend/src/components/OrganizationSelectList.tsx | 3 ++- frontend/src/components/SharedUsersLists.tsx | 3 ++- frontend/src/components/SortServices.tsx | 7 +++---- frontend/src/components/Tags.tsx | 6 ++---- frontend/src/helpers/selectedHelper.ts | 3 ++- frontend/src/helpers/utilHelper.ts | 14 ++++++++++++++ frontend/src/models/connections.ts | 7 ++----- frontend/src/models/search.ts | 7 ++----- frontend/src/pages/NetworksPage.tsx | 3 ++- frontend/src/selectors/contacts.ts | 3 ++- frontend/src/selectors/tags.ts | 3 ++- 15 files changed, 43 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/FilterDrawer.tsx b/frontend/src/components/FilterDrawer.tsx index cb2d08980..96d00641d 100644 --- a/frontend/src/components/FilterDrawer.tsx +++ b/frontend/src/components/FilterDrawer.tsx @@ -9,6 +9,7 @@ import { TagFilterToggle } from './TagFilterToggle' import { FilterSelector } from './FilterSelector' import { AccordionMenu } from './AccordionMenu' import { selectTags } from '../selectors/tags' +import { byName } from '../helpers/utilHelper' import { useLabel } from '../hooks/useLabel' import { Drawer } from './Drawer' @@ -135,7 +136,7 @@ export const FilterDrawer: React.FC = () => { filterList={platformFilter.concat( Object.keys(platforms.nameLookup) .map(p => ({ value: parseInt(p), name: platforms.nameLookup[p] })) - .sort((a, b) => (a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1)) + .sort(byName) )} /> ), diff --git a/frontend/src/components/OrganizationGuestList.tsx b/frontend/src/components/OrganizationGuestList.tsx index 41253917b..d1eb3b069 100644 --- a/frontend/src/components/OrganizationGuestList.tsx +++ b/frontend/src/components/OrganizationGuestList.tsx @@ -6,6 +6,7 @@ import { useGuests } from '../hooks/useGuests' import { LoadingMessage } from './LoadingMessage' import { Pagination } from '@mui/lab' import { Gutters } from './Gutters' +import { alphaSort } from '../helpers/utilHelper' import { Avatar } from './Avatar' import { Icon } from './Icon' @@ -15,7 +16,7 @@ export const OrganizationGuestList: React.FC = () => { const perPage = 20 const pageCount = Math.ceil(guests.length / perPage) const start = (page - 1) * perPage - const pageGuests = [...guests].sort(alphaEmailSort).slice(start, start + perPage) + const pageGuests = [...guests].sort((a, b) => alphaSort(a.email, b.email)).slice(start, start + perPage) if (!guestsLoaded) return @@ -70,8 +71,3 @@ export const OrganizationGuestList: React.FC = () => { ) } -function alphaEmailSort(a, b) { - const aa = a.email.toLowerCase() - const bb = b.email.toLowerCase() - return aa > bb ? 1 : aa < bb ? -1 : 0 -} diff --git a/frontend/src/components/OrganizationMemberList.tsx b/frontend/src/components/OrganizationMemberList.tsx index a240307c1..76ba7f5bc 100644 --- a/frontend/src/components/OrganizationMemberList.tsx +++ b/frontend/src/components/OrganizationMemberList.tsx @@ -2,6 +2,7 @@ import React from 'react' import { useSelector } from 'react-redux' import { selectAvailableUsers } from '../selectors/organizations' import { OrganizationMember } from '../components/OrganizationMember' +import { alphaSort } from '../helpers/utilHelper' import { IOrganizationState } from '../models/organization' import { List } from '@mui/material' @@ -9,7 +10,7 @@ type Props = { organization?: IOrganizationState; owner?: IOrganizationMember; e export const OrganizationMemberList: React.FC = ({ organization, owner, enterprise }) => { const freeUsers = useSelector(selectAvailableUsers) - const members = organization?.members ? [...organization.members].sort(alphaEmailSort) : [] + const members = organization?.members ? [...organization.members].sort((a, b) => alphaSort(a.user.email, b.user.email)) : [] return ( {owner && ( @@ -35,8 +36,3 @@ export const OrganizationMemberList: React.FC = ({ organization, owner, e ) } -function alphaEmailSort(a, b) { - const aa = a.user.email.toLowerCase() - const bb = b.user.email.toLowerCase() - return aa > bb ? 1 : aa < bb ? -1 : 0 -} diff --git a/frontend/src/components/OrganizationSelect.tsx b/frontend/src/components/OrganizationSelect.tsx index 1d86c2d83..4a4c77c49 100644 --- a/frontend/src/components/OrganizationSelect.tsx +++ b/frontend/src/components/OrganizationSelect.tsx @@ -10,6 +10,7 @@ import { selectAllConnectionSessions } from '../selectors/connections' import { selectOrganization } from '../selectors/organizations' import { GuideBubble } from './GuideBubble' import { fontSizes } from '../styling' +import { byName } from '../helpers/utilHelper' import { Avatar } from './Avatar' import { Icon } from './Icon' @@ -146,7 +147,7 @@ export const OrganizationSelect: React.FC = () => { } } - options.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)) + options.sort(byName) if (!options.length) return null const mySessions = sessions.filter(s => s.target.accountId === ownOrg?.id).length diff --git a/frontend/src/components/OrganizationSelectList.tsx b/frontend/src/components/OrganizationSelectList.tsx index 0ce4334a7..2fff0ece6 100644 --- a/frontend/src/components/OrganizationSelectList.tsx +++ b/frontend/src/components/OrganizationSelectList.tsx @@ -7,6 +7,7 @@ import { ListItemButton, ListSubheader, ListItemIcon, ListItemText, Chip } from import { getOwnOrganization } from '../models/organization' import { selectOrganization } from '../selectors/organizations' import { IconButton } from '../buttons/IconButton' +import { byName } from '../helpers/utilHelper' import { Avatar } from './Avatar' const AVATAR_SIZE = 28 @@ -45,7 +46,7 @@ export const OrganizationSelectList: React.FC = () => { } } - options.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)) + options.sort(byName) if (!options.length) return null return ( diff --git a/frontend/src/components/SharedUsersLists.tsx b/frontend/src/components/SharedUsersLists.tsx index 3ead0625c..d2870f729 100644 --- a/frontend/src/components/SharedUsersLists.tsx +++ b/frontend/src/components/SharedUsersLists.tsx @@ -9,6 +9,7 @@ import { selectMembersWithAccess } from '../selectors/organizations' import { selectOrganization } from '../selectors/organizations' import { ShareButton } from '../buttons/ShareButton' import { IconButton } from '../buttons/IconButton' +import { alphaSort } from '../helpers/utilHelper' import { Gutters } from './Gutters' import { Icon } from './Icon' @@ -93,4 +94,4 @@ export const SharedUsersLists: React.FC = ({ device, network, connected = ) } -const sort = (users: IUser[]) => users.sort((a, b) => (a.email > b.email ? 1 : b.email > a.email ? -1 : 0)) +const sort = (users: IUser[]) => users.sort((a, b) => alphaSort(a.email, b.email)) diff --git a/frontend/src/components/SortServices.tsx b/frontend/src/components/SortServices.tsx index 487d7304a..3e3e46612 100644 --- a/frontend/src/components/SortServices.tsx +++ b/frontend/src/components/SortServices.tsx @@ -3,6 +3,7 @@ import { selectDeviceModelAttributes } from '../selectors/devices' import { IconButton, Menu, MenuItem } from '@mui/material' import { useDispatch, useSelector } from 'react-redux' import { State, Dispatch } from '../store' +import { byName } from '../helpers/utilHelper' import { Icon } from './Icon' export function getSortOptions(key: ISortServiceType) { @@ -13,14 +14,12 @@ export function getSortOptions(key: ISortServiceType) { const optionSortServices: IOptionServiceSort = { ATOZ: { name: 'Alpha A-Z', - sortService: (a: IService, b: IService) => - a.name.toLowerCase() > b.name.toLowerCase() ? 1 : a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 0, + sortService: byName, icon: 'sort-alpha-down', }, ZTOA: { name: 'Alpha Z-A', - sortService: (a: IService, b: IService) => - a.name.toLowerCase() < b.name.toLowerCase() ? 1 : a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 0, + sortService: (a: IService, b: IService) => byName(b, a), icon: 'sort-alpha-up', }, NEWEST: { diff --git a/frontend/src/components/Tags.tsx b/frontend/src/components/Tags.tsx index 3a01078e9..aca96a25b 100644 --- a/frontend/src/components/Tags.tsx +++ b/frontend/src/components/Tags.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from 'react' import { Chip, BoxProps, Typography } from '@mui/material' +import { byName } from '../helpers/utilHelper' import { Tag } from './Tag' export type TagProps = BoxProps & { @@ -14,7 +15,7 @@ export type TagProps = BoxProps & { export const Tags: React.FC = ({ tags, small, max = 1, showEmpty, hideLabels, onClick, onDelete }) => { const dot = tags.length > max && small - const sortedTags = useMemo(() => [...tags].sort(nameSort), [tags]) + const sortedTags = useMemo(() => [...tags].sort(byName), [tags]) if (!tags.length && showEmpty) return ( @@ -37,6 +38,3 @@ export const Tags: React.FC = ({ tags, small, max = 1, showEmpty, hide return <>{dot ? : tagElements} } -function nameSort(a: ITag, b: ITag) { - return a.name.localeCompare(b.name) -} diff --git a/frontend/src/helpers/selectedHelper.ts b/frontend/src/helpers/selectedHelper.ts index 991771fe0..c185d8277 100644 --- a/frontend/src/helpers/selectedHelper.ts +++ b/frontend/src/helpers/selectedHelper.ts @@ -1,5 +1,6 @@ import structuredClone from '@ungap/structured-clone' import { getDevices } from '../selectors/devices' +import { byName } from './utilHelper' import { State } from '../store' export function eachSelectedDevice(state: State, selected: IDevice['id'][], callback: (device: IDevice) => void) { @@ -17,5 +18,5 @@ export function getSelectedTags(devices?: IDevice[], selected?: IDevice['id'][]) }) } }) - return result.sort((a, b) => a.name.localeCompare(b.name)) + return result.sort(byName) } diff --git a/frontend/src/helpers/utilHelper.ts b/frontend/src/helpers/utilHelper.ts index 6bca906b2..432f5cbbe 100644 --- a/frontend/src/helpers/utilHelper.ts +++ b/frontend/src/helpers/utilHelper.ts @@ -1,5 +1,19 @@ import { REGEX_VALID_IP } from '../constants' +// One comparator behind every alphabetical sort in the UI, so "alphabetical" means the same +// thing everywhere: numeric so device2 sorts before device10, base sensitivity so case and +// accents don't split otherwise equal names. Held as a collator because localeCompare with an +// options object builds a new one on every call. +const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) + +export function alphaSort(a: string = '', b: string = '') { + return collator.compare(a, b) +} + +export function byName(a: T, b: T) { + return alphaSort(a.name, b.name) +} + export function toLookup(array: T[], key: string): ILookup { return array.reduce((obj, item) => ({ ...obj, [item[key]]: item }), {}) } diff --git a/frontend/src/models/connections.ts b/frontend/src/models/connections.ts index 42d95053a..9f3dc1afa 100644 --- a/frontend/src/models/connections.ts +++ b/frontend/src/models/connections.ts @@ -3,7 +3,7 @@ import browser from '../services/browser' import structuredClone from '@ungap/structured-clone' import { createModel } from '@rematch/core' import { parse as urlParse } from 'url' -import { pickTruthy } from '../helpers/utilHelper' +import { alphaSort, pickTruthy } from '../helpers/utilHelper' import { DEFAULT_CONNECTION, IP_PRIVATE } from '@common/constants' import { REGEX_HIDDEN_PASSWORD, CERTIFICATE_DOMAIN } from '../constants' import { @@ -570,7 +570,7 @@ export default createModel()({ }, async setAll(all: IConnection[]) { - all.sort((a, b) => nameSort(a.name || '', b.name || '')) + all.sort((a, b) => alphaSort(a.name, b.name)) dispatch.connections.set({ all: [...all] }) // to ensure we trigger update }, }), @@ -593,6 +593,3 @@ export default createModel()({ }, }) -function nameSort(a: string, b: string) { - return a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0 -} diff --git a/frontend/src/models/search.ts b/frontend/src/models/search.ts index 61b387d93..5798b2bde 100644 --- a/frontend/src/models/search.ts +++ b/frontend/src/models/search.ts @@ -4,6 +4,7 @@ import { removeDeviceName } from '@common/nameHelper' import { graphQLBasicRequest } from '../services/graphQL' import { selectActiveAccountId } from '../selectors/accounts' import { selectDeviceModelAttributes } from '../selectors/devices' +import { alphaSort } from '../helpers/utilHelper' import { RootModel } from '.' type ISearchState = ILookup & { @@ -156,10 +157,6 @@ export default createModel()({ }) export function sortSearch(search: ISearch[]): ISearch[] { - const sorted = search.sort((a, b) => { - if (a.nodeName.toLowerCase() > b.nodeName.toLowerCase()) return 1 - if (a.nodeName.toLowerCase() < b.nodeName.toLowerCase()) return -1 - return 0 - }) + const sorted = search.sort((a, b) => alphaSort(a.nodeName, b.nodeName)) return sorted || [] } diff --git a/frontend/src/pages/NetworksPage.tsx b/frontend/src/pages/NetworksPage.tsx index 91069559f..3d50c034a 100644 --- a/frontend/src/pages/NetworksPage.tsx +++ b/frontend/src/pages/NetworksPage.tsx @@ -13,13 +13,14 @@ import { IconButton } from '../buttons/IconButton' import { Container } from '../components/Container' import { Network } from '../components/Network' import { Gutters } from '../components/Gutters' +import { byName } from '../helpers/utilHelper' import { Title } from '../components/Title' import { Icon } from '../components/Icon' export const NetworksPage: React.FC = () => { const { t } = useTranslation() const dispatch = useDispatch() - const all = [...useSelector(selectNetworks)].sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)) + const all = [...useSelector(selectNetworks)].sort(byName) const initialized = useSelector((state: State) => state.networks.initialized) const permissions = useSelector(selectPermissions) const loading = useSelector(selectDeviceModelAttributes).fetching diff --git a/frontend/src/selectors/contacts.ts b/frontend/src/selectors/contacts.ts index 10de470e6..47ffde2b3 100644 --- a/frontend/src/selectors/contacts.ts +++ b/frontend/src/selectors/contacts.ts @@ -1,6 +1,7 @@ import { createSelector } from 'reselect' import { State } from '../store' import { selectOrganization } from './organizations' +import { alphaSort } from '../helpers/utilHelper' import { isUserAccount } from './accounts' const getContacts = (state: State) => state.contacts.all @@ -24,6 +25,6 @@ export const selectContacts = createSelector( seen.add(key) return true }) - .sort((a, b) => a.email.localeCompare(b.email, undefined, { sensitivity: 'base' })) + .sort((a, b) => alphaSort(a.email, b.email)) } ) diff --git a/frontend/src/selectors/tags.ts b/frontend/src/selectors/tags.ts index b2d354af7..c40c3cc8c 100644 --- a/frontend/src/selectors/tags.ts +++ b/frontend/src/selectors/tags.ts @@ -1,7 +1,8 @@ import { getTags } from './state' import { createSelector } from 'reselect' import { selectActiveAccountId } from './accounts' +import { byName } from '../helpers/utilHelper' export const selectTags = createSelector([getTags, selectActiveAccountId], (tags, accountId) => - [...(tags[accountId] || [])].sort((a, b) => a.name.localeCompare(b.name)) + [...(tags[accountId] || [])].sort(byName) )