diff --git a/frontend/src/lib/components/client-provider.vue b/frontend/src/lib/components/client-provider.vue index e0c4f77b..31270a5c 100644 --- a/frontend/src/lib/components/client-provider.vue +++ b/frontend/src/lib/components/client-provider.vue @@ -74,10 +74,10 @@ if (lastMapId && bookmark.id == lastMapId) bookmark.id = newClient.mapId!; - if (lastMapData && lastMapData.id == bookmark.padId) - bookmark.padId = newClient.mapData!.id; + if (lastMapData && lastMapData.id == bookmark.mapId) + bookmark.mapId = newClient.mapData!.id; - if (bookmark.padId == newClient.mapData!.id) + if (bookmark.mapId == newClient.mapData!.id) bookmark.name = newClient.mapData!.name; } diff --git a/frontend/src/lib/components/manage-bookmarks-dialog.vue b/frontend/src/lib/components/manage-bookmarks-dialog.vue index d83bd543..a984dab8 100644 --- a/frontend/src/lib/components/manage-bookmarks-dialog.vue +++ b/frontend/src/lib/components/manage-bookmarks-dialog.vue @@ -26,7 +26,7 @@ } function addBookmark(): void { - storage.bookmarks.push({ id: client.value.mapId!, padId: client.value.mapData!.id, name: client.value.mapData!.name }); + storage.bookmarks.push({ id: client.value.mapId!, mapId: client.value.mapData!.id, name: client.value.mapData!.name }); } diff --git a/frontend/src/lib/components/toolbox/toolbox-collab-maps-dropdown.vue b/frontend/src/lib/components/toolbox/toolbox-collab-maps-dropdown.vue index b3d58eb2..a8c01087 100644 --- a/frontend/src/lib/components/toolbox/toolbox-collab-maps-dropdown.vue +++ b/frontend/src/lib/components/toolbox/toolbox-collab-maps-dropdown.vue @@ -34,7 +34,7 @@ }); function addBookmark(): void { - storage.bookmarks.push({ id: client.value.mapId!, padId: client.value.mapData!.id, name: client.value.mapData!.name }); + storage.bookmarks.push({ id: client.value.mapId!, mapId: client.value.mapData!.id, name: client.value.mapData!.name }); } diff --git a/frontend/src/lib/utils/__tests__/storage.test.ts b/frontend/src/lib/utils/__tests__/storage.test.ts new file mode 100644 index 00000000..9a3370ff --- /dev/null +++ b/frontend/src/lib/utils/__tests__/storage.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from "vitest"; +import { storageValidator } from "../storage"; + +describe("storageValidator", () => { + test("default values", () => { + expect(storageValidator.parse(undefined)).toEqual({ + zoomToAll: false, + autoZoom: true, + bookmarks: [] + }); + + expect(storageValidator.parse({})).toEqual({ + zoomToAll: false, + autoZoom: true, + bookmarks: [] + }); + }); + + test("fallback values", () => { + expect(storageValidator.parse({ + zoomToAll: "invalid", + autoZoom: "invalid", + bookmarks: "invalid" + })).toEqual({ + zoomToAll: false, + autoZoom: true, + bookmarks: [] + }); + }); + + test("valid values", () => { + const value = { + zoomToAll: true, + autoZoom: false, + bookmarks: [ + { id: "adminId1", mapId: "readId1", name: "Test map" }, + { id: "adminId2", mapId: "readId2", name: "Test map", customName: "Custom name" } + ] + }; + + expect(storageValidator.parse(value)).toEqual(value); + }); + + test("invalid bookmark", () => { + const bookmark1 = { id: "adminId1", mapId: "readId1", name: "Test map" }; + const bookmark2 = "invalid"; + const bookmark3 = { id: "adminId2", mapId: "readId2", name: "Test map", customName: "Custom name" } + + expect(storageValidator.parse({ + bookmarks: [bookmark1, bookmark2, bookmark3] + })).toMatchObject({ + bookmarks: [bookmark1, bookmark3] + }); + }); + + test("legacy bookmark", () => { + expect(storageValidator.parse({ + bookmarks: [ + { id: "adminId1", mapId: "readId1", name: "Test map" }, + { id: "adminId2", padId: "readId2", name: "Test map", customName: "Custom name" } + ] + })).toMatchObject({ + bookmarks: [ + { id: "adminId1", mapId: "readId1", name: "Test map" }, + { id: "adminId2", mapId: "readId2", name: "Test map", customName: "Custom name" } + ] + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/lib/utils/storage.ts b/frontend/src/lib/utils/storage.ts index b66535df..56f02591 100644 --- a/frontend/src/lib/utils/storage.ts +++ b/frontend/src/lib/utils/storage.ts @@ -1,53 +1,83 @@ -import type { MapId } from "facilmap-types"; +import { mapIdValidator } from "facilmap-types"; +import { overwriteObject } from "facilmap-utils"; import { isEqual } from "lodash-es"; import { reactive, watch } from "vue"; +import * as z from "zod"; -export interface Bookmark { - /** ID used to open the map */ - id: MapId; - /** Read-only ID of the map */ - padId: MapId; - /** Last known name of the map */ - name: string; - /** If this is defined, it is shown instead of the map name. */ - customName?: string; +function arrayIgnoringInvalids(schema: T): z.ZodType>> { + return z.array(z.any()).transform((arr): Array> => { + return arr.flatMap((v) => { + const parsed = schema.safeParse(v); + if (parsed.success) { + return [parsed.data]; + } else { + return []; + } + }); + }); } +export const bookmarkValidator = z.record(z.any()).transform((val) => ({ + ...val, + mapId: val.mapId ?? val.padId // padId is the legacy property name +})).pipe(z.object({ + /** ID used to open the map */ + id: mapIdValidator, + /** Read-only ID of the map */ + mapId: mapIdValidator, + /** Last known name of the map */ + name: z.string(), + /** If this is defined, it is shown instead of the map name. */ + customName: z.string().optional() +})); + +export type Bookmark = z.infer; + +export const storageValidator = z.record(z.any()).catch(() => ({})).pipe(z.object({ + zoomToAll: z.boolean().catch(false), + autoZoom: z.boolean().catch(true), + bookmarks: arrayIgnoringInvalids(bookmarkValidator).catch(() => []) +})); + export interface Storage { zoomToAll: boolean; autoZoom: boolean; bookmarks: Bookmark[]; } -const storage: Storage = reactive({ - zoomToAll: false, - autoZoom: true, - bookmarks: [] -}); +const storage: Storage = reactive(storageValidator.parse({})); export default storage; -function load(): void { +function parseStorage(logErrors = false): Storage { try { - const val = localStorage.getItem("facilmap"); + let val = localStorage.getItem("facilmap"); if (val) { - const parsed = JSON.parse(val); - storage.zoomToAll = !!parsed.zoomToAll; - storage.autoZoom = !!parsed.autoZoom; - storage.bookmarks = parsed.bookmarks || []; + val = JSON.parse(val); } + + return storageValidator.parse(val); } catch (err) { - console.error("Error reading local storage", err); + if (logErrors) { + console.error("Error reading local storage", err); + } + return storageValidator.parse({}); } } +function load(): void { + const val = parseStorage(true); + overwriteObject(val, storage); +} + async function save() { try { - const currentItem = JSON.parse(localStorage.getItem("facilmap") || "null"); - if (!currentItem || !isEqual(currentItem, storage)) { + const currentItem = parseStorage(); + + if (!isEqual(currentItem, storage)) { localStorage.setItem("facilmap", JSON.stringify(storage)); - if (storage.bookmarks.length > 0 && !isEqual(currentItem?.bookmarks, storage.bookmarks) && navigator.storage?.persist) + if (storage.bookmarks.length > 0 && !isEqual(currentItem.bookmarks, storage.bookmarks) && navigator.storage?.persist) await navigator.storage.persist(); } } catch (err) { diff --git a/leaflet/src/bbox-handler.ts b/leaflet/src/bbox-handler.ts index 45d10679..4d0f91a0 100644 --- a/leaflet/src/bbox-handler.ts +++ b/leaflet/src/bbox-handler.ts @@ -40,7 +40,6 @@ export default class BboxHandler extends Handler { } shouldUpdateBbox(): boolean { - console.log(!!this.client.mapData, !!this.client.route, Object.keys(this.client.routes).length > 0); return !!this.client.mapData || !!this.client.route || Object.keys(this.client.routes).length > 0; }