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