mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(planner): road trip mode, the dawarich trail and the route overview follow the account to another browser
The three planner view switches lived in sessionStorage only, so a new tab, another browser or another device opened every trip on the plain view again. They are now kept per trip in one user setting, planner_trip_views, validated against a contract in @trek/shared. The server keeps only switches that are on and only trips the user can still open, so a deleted trip or one the user left drops out on the next write. The tab copy in sessionStorage stays and answers until the settings have loaded. Refs #2639
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
import { settingUpsertRequestSchema, settingsBulkRequestSchema, MASKED_SETTING_VALUE } from './settings.schema';
|
||||
import {
|
||||
settingUpsertRequestSchema,
|
||||
settingsBulkRequestSchema,
|
||||
MASKED_SETTING_VALUE,
|
||||
PLANNER_TRIP_VIEWS_MAX,
|
||||
compactPlannerTripViews,
|
||||
plannerTripViewsSchema,
|
||||
} from './settings.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
@@ -23,3 +30,36 @@ describe('MASKED_SETTING_VALUE', () => {
|
||||
expect(MASKED_SETTING_VALUE).toBe('••••••••');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plannerTripViewsSchema (#2639)', () => {
|
||||
it('takes the three switches per trip id', () => {
|
||||
expect(
|
||||
plannerTripViewsSchema.safeParse({ '12': { roadtrip: true, dawarich: false, overview: true } }).success,
|
||||
).toBe(true);
|
||||
expect(plannerTripViewsSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
|
||||
it('refuses a key that is not a trip id and a switch that is not a boolean', () => {
|
||||
expect(plannerTripViewsSchema.safeParse({ abc: { roadtrip: true } }).success).toBe(false);
|
||||
expect(plannerTripViewsSchema.safeParse({ '0': { roadtrip: true } }).success).toBe(false);
|
||||
expect(plannerTripViewsSchema.safeParse({ '12': { roadtrip: 'yes' } }).success).toBe(false);
|
||||
expect(plannerTripViewsSchema.safeParse(['12']).success).toBe(false);
|
||||
});
|
||||
|
||||
it('refuses more trips than the row is meant to hold', () => {
|
||||
const many = Object.fromEntries(
|
||||
Array.from({ length: PLANNER_TRIP_VIEWS_MAX + 1 }, (_, i) => [String(i + 1), { roadtrip: true }]),
|
||||
);
|
||||
expect(plannerTripViewsSchema.safeParse(many).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compactPlannerTripViews', () => {
|
||||
it('keeps only the switches that are on and drops trips with none', () => {
|
||||
expect(
|
||||
compactPlannerTripViews({ '1': { roadtrip: true, dawarich: false }, '2': { overview: false }, '3': {} }),
|
||||
).toEqual({
|
||||
'1': { roadtrip: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,3 +66,43 @@ export type SettingsBulkResponse = z.infer<typeof settingsBulkResponseSchema>;
|
||||
/** GET/PUT /api/admin/default-user-settings: the stored defaults a new account starts with, by key. */
|
||||
export const adminDefaultUserSettingsResponseSchema = z.record(z.string(), z.unknown());
|
||||
export type AdminDefaultUserSettingsResponse = z.infer<typeof adminDefaultUserSettingsResponseSchema>;
|
||||
|
||||
/**
|
||||
* The planner's view switches a traveller expects to find again on any device
|
||||
* (#2639): road trip mode, the recorded Dawarich trail and the whole-trip route
|
||||
* overview. One settings row per user, keyed by trip id, holding only the
|
||||
* switches that are on; a trip without any is left out instead of stored as a
|
||||
* row of falses. The server drops trips the user can no longer open on every
|
||||
* write, so a deleted trip does not linger in the row.
|
||||
*/
|
||||
export const PLANNER_TRIP_VIEWS_KEY = 'planner_trip_views';
|
||||
/** More trips than anyone keeps open at once; it bounds the row, not a person. */
|
||||
export const PLANNER_TRIP_VIEWS_MAX = 500;
|
||||
|
||||
export const plannerTripViewSchema = z.object({
|
||||
roadtrip: z.boolean().optional(),
|
||||
dawarich: z.boolean().optional(),
|
||||
overview: z.boolean().optional(),
|
||||
});
|
||||
export type PlannerTripView = z.infer<typeof plannerTripViewSchema>;
|
||||
export type PlannerTripViewFlag = keyof PlannerTripView;
|
||||
|
||||
export const plannerTripViewsSchema = z
|
||||
.record(z.string().regex(/^[1-9]\d{0,9}$/), plannerTripViewSchema)
|
||||
.refine((views) => Object.keys(views).length <= PLANNER_TRIP_VIEWS_MAX, {
|
||||
message: `At most ${PLANNER_TRIP_VIEWS_MAX} trips`,
|
||||
});
|
||||
export type PlannerTripViews = z.infer<typeof plannerTripViewsSchema>;
|
||||
|
||||
/** The views with every trip that has no switch on left out. */
|
||||
export function compactPlannerTripViews(views: PlannerTripViews): PlannerTripViews {
|
||||
const kept: PlannerTripViews = {};
|
||||
for (const [tripId, view] of Object.entries(views)) {
|
||||
const on: PlannerTripView = {};
|
||||
if (view.roadtrip) on.roadtrip = true;
|
||||
if (view.dawarich) on.dawarich = true;
|
||||
if (view.overview) on.overview = true;
|
||||
if (Object.keys(on).length > 0) kept[tripId] = on;
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user