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
@@ -1,6 +1,6 @@
import { readPbfFile, type OsmWay } from './osm-pbf.reader';
import { keptTags, paddleKindOf, paddleNameOf } from './paddle-tags';
import type { PaddleGeometry, PaddleObstacleKind } from '@trek/shared';
import { compareCodeUnits, type PaddleGeometry, type PaddleObstacleKind } from '@trek/shared';
import { createHash } from 'node:crypto';
import { stat } from 'node:fs/promises';
@@ -38,7 +38,7 @@ interface PendingWay {
function hashOf(tags: Record<string, string>, geometry: PaddleGeometry): string {
const sorted = Object.keys(tags)
.sort()
.sort(compareCodeUnits)
.map((key) => [key, tags[key]]);
const rounded = JSON.stringify(geometry, (_key, value: unknown) =>
typeof value === 'number' ? Math.round(value * 1e6) / 1e6 : value,
@@ -147,7 +147,7 @@ export function groupLockParts(features: ExtractedFeature[]): void {
for (const part of parts) groups.set(root(part), [...(groups.get(root(part)) ?? []), part]);
for (const members of groups.values()) {
if (members.length < 2) continue;
const key = `lock:${members.map((member) => member.osm_ref).sort()[0]}`;
const key = `lock:${members.map((member) => member.osm_ref).sort(compareCodeUnits)[0]}`;
for (const member of members) member.group_key = key;
}
}
@@ -17,6 +17,7 @@ import { parseRouteGeometry, shortHash } from './tours.helpers';
import { InjectRepository } from '@mikro-orm/nestjs';
import { Injectable } from '@nestjs/common';
import {
compareCodeUnits,
obstacleCandidatesOf,
obstacleEntriesOf,
ownEntriesOf,
@@ -66,7 +67,7 @@ export function storedReviewOf(row: TourObstacleReviewRow): StoredObstacleReview
}
function oldest(values: ReadonlyArray<string | null>): string | null {
return values.filter((value): value is string => !!value).sort()[0] ?? null;
return values.filter((value): value is string => !!value).sort(compareCodeUnits)[0] ?? null;
}
const UNAVAILABLE = { data: 'unavailable', data_as_of: null, read_at: null } as const;
@@ -126,7 +127,7 @@ export class TourPaddleCheckService {
line,
candidates,
own,
dataKeys: new Set(along.features.map(paddleObstacleKeyOf)),
dataKeys: new Set(along.features.map((feature) => paddleObstacleKeyOf(feature))),
complete: along.coverage === 'covered' && !limited,
data: {
data: along.coverage === 'none' ? 'none' : limited ? 'limited' : along.coverage,
+4 -1
View File
@@ -62,6 +62,9 @@ export interface ImportGpxAsTourResult {
* PlacesService.importPreparedGpx. Places and facet rows are persisted in one transaction,
* with tour metrics derived from each place's route_geometry.
*/
/** A GPX file imported without a type is a hike. */
const HIKE_IMPORT: TourGpxImportFields = { tour_type: 'hike' };
@Injectable()
export class ToursService {
private readonly logger = new Logger(ToursService.name);
@@ -522,7 +525,7 @@ export class ToursService {
fileBuffer: Buffer,
fileName?: string,
socketId?: string,
fields: TourGpxImportFields = { tour_type: 'hike' },
fields: TourGpxImportFields = HIKE_IMPORT,
): Promise<ImportGpxAsTourResult | null> {
const rows = this.readGpxTracks(fileBuffer, fileName);
if (rows.length === 0) return null;