fix(tours): sorts that name their order, the note focused without autofocus

The checklist and the obstacle data sorted texts without a compare
function. They now sort by code units through one shared compareCodeUnits,
the order a bare sort gave, the same on every machine; the material data of
a lock's parts no longer orders by the server's locale, so its hash cannot
change with it. The note editor takes the focus through a ref instead of
autoFocus, a direct function reference in a map becomes a call, and the
GPX import's default fields are a named constant.
This commit is contained in:
Maurice
2026-10-17 03:18:04 +00:00
parent 9272151c07
commit 09caf4bc3f
10 changed files with 48 additions and 13 deletions
+13
View File
@@ -0,0 +1,13 @@
import { compareCodeUnits } from './compare';
import { describe, expect, it } from 'vitest';
describe('compareCodeUnits', () => {
it('orders as a bare sort does, whatever the locale', () => {
const texts = ['way/3', 'node/10', 'node/2', 'Node/1', 'check_date', 'canoe', '2026-09-30', '2026-10-01'];
expect([...texts].sort(compareCodeUnits)).toEqual([...texts].sort());
expect(compareCodeUnits('a', 'a')).toBe(0);
expect(compareCodeUnits('a', 'b')).toBe(-1);
expect(compareCodeUnits('b', 'a')).toBe(1);
});
});
+9
View File
@@ -0,0 +1,9 @@
/**
* Two texts by their UTF-16 code units: the order a bare `sort()` gives, and
* the same on every machine. A hash or a key built over the order must not
* depend on the locale the code runs in, as `localeCompare` does.
*/
export function compareCodeUnits(a: string, b: string): number {
if (a === b) return 0;
return a < b ? -1 : 1;
}
+1
View File
@@ -10,6 +10,7 @@
*/
export * from './common/primitives.schema';
export * from './common/pagination.schema';
export * from './common/compare';
// Domain contracts
export * from './weather/weather.schema';
+3 -2
View File
@@ -1,3 +1,4 @@
import { compareCodeUnits } from '../common/compare';
import { haversineMetres } from '../geo/haversine';
import type { PaddleFeature } from '../paddle/paddle-obstacles.schema';
import { paddleLayoutOfPoints, paddleLayoutOfTrack, type PaddlePlanPoint } from './paddle-plan';
@@ -214,14 +215,14 @@ const isCosmetic = (key: string) => COSMETIC.has(key) || key.startsWith('check_d
/** The material data of an obstacle's members, as text to hash: what they are and the tags that bear on passing. */
export function materialOf(members: readonly PaddleFeature[]): string {
const rows = [...members]
.sort((a, b) => a.ref.localeCompare(b.ref))
.sort((a, b) => compareCodeUnits(a.ref, b.ref))
.map((member) => [
member.ref,
member.kind,
member.geometry.type,
Object.keys(member.tags)
.filter((key) => !isCosmetic(key))
.sort()
.sort(compareCodeUnits)
.map((key) => [key, member.tags[key]]),
]);
return JSON.stringify(rows);
+3 -2
View File
@@ -1,3 +1,4 @@
import { compareCodeUnits } from '../common/compare';
import {
PADDLE_BLOCKING_KINDS,
paddleAccessOf,
@@ -166,7 +167,7 @@ function localCheckOf(members: readonly PaddleFeature[]): string | null {
)
.map(([, value]) => value),
);
const sorted = dates.sort();
const sorted = dates.sort(compareCodeUnits);
return sorted[sorted.length - 1] ?? null;
}
@@ -239,7 +240,7 @@ export function obstacleCandidatesOf(input: {
...members
.map((member) => member.ref)
.filter((ref) => ref !== anchor.feature.ref)
.sort(),
.sort(compareCodeUnits),
].slice(0, OBSTACLE_REFS_MAX),
kind,
name: anchor.feature.name ?? members.find((member) => member.name)?.name ?? null,