Skip to content

Commit

Permalink
localstorage: add schema support
Browse files Browse the repository at this point in the history
  • Loading branch information
MrFlashAccount committed May 22, 2024
1 parent 753ef5f commit 9331e69
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 41 deletions.
14 changes: 14 additions & 0 deletions app/ide-desktop/lib/dashboard/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,18 @@ async function mockDate({ page }: MockParams) {
}`)
}

/**
* Passes Terms and conditions dialog
* @param page
*/
export async function passTermsAndConditionsDialog({page}: MockParams) {
await page.waitForSelector('', {
state: 'attached'
})


}

// ========================
// === mockIDEContainer ===
// ========================
Expand Down Expand Up @@ -836,8 +848,10 @@ export async function mockAll({ page }: MockParams) {
export async function mockAllAndLogin({ page }: MockParams) {
const mocks = await mockAll({ page })
await login({ page })

// This MUST run after login, otherwise the element's styles are reset when the browser
// is navigated to another page.
await mockIDEContainer({ page })

return mocks
}
44 changes: 7 additions & 37 deletions app/ide-desktop/lib/dashboard/src/modals/TermsOfServiceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as React from 'react'
import * as reactQuery from '@tanstack/react-query'
import * as router from 'react-router'
import * as twMerge from 'tailwind-merge'
import invariant from 'tiny-invariant'
import * as z from 'zod'

import * as authProvider from '#/providers/AuthProvider'
import * as localStorageProvider from '#/providers/LocalStorageProvider'
Expand All @@ -18,43 +18,18 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import * as ariaComponents from '#/components/AriaComponents'

import type * as localStorage from '#/utilities/LocalStorage'
import LocalStorage from '#/utilities/LocalStorage'

declare module '#/utilities/LocalStorage' {
/**
* The data stored in the `termsOfService` key.
*/
interface AcceptedTosData {
/**
* The hash of the latest terms of service version that the user has accepted.
*/
readonly versionHash: string
}

/**
* Contains the latest terms of service version hash that the user has accepted.
*/
interface LocalStorageData {
readonly termsOfService: AcceptedTosData | null
readonly termsOfService: z.infer<typeof TERMS_OF_SERVICE_SCHEMA> | null
}
}

LocalStorage.registerKey('termsOfService', {
tryParse: value => {
if (
typeof value === 'object' &&
value != null &&
'versionHash' in value &&
typeof value.versionHash === 'string'
) {
// eslint-disable-next-line no-restricted-syntax
return value as localStorage.LocalStorageData['termsOfService']
} else {
return null
}
},
})
const TERMS_OF_SERVICE_SCHEMA = z.object({ versionHash: z.string() })
LocalStorage.registerKey('termsOfService', { schema: TERMS_OF_SERVICE_SCHEMA })

export const latestTermsOfService = reactQuery.queryOptions({
queryKey: ['termsOfService', 'currentVersion'],
Expand All @@ -67,14 +42,9 @@ export const latestTermsOfService = reactQuery.queryOptions({
return response.json()
}
})
.then((data: unknown) => {
invariant(data != null && typeof data === 'object', 'Invalid terms of service response')
invariant(
'hash' in data && typeof data.hash === 'string',
'Invalid terms of service response'
)

return { hash: data.hash }
.then(data => {
const schema = z.object({ hash: z.string() })
return schema.parse(data)
}),
refetchOnWindowFocus: true,
refetchIntervalInBackground: true,
Expand Down
39 changes: 35 additions & 4 deletions app/ide-desktop/lib/dashboard/src/utilities/LocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/** @file A LocalStorage data manager. */
import type * as z from 'zod'

import * as common from 'enso-common'

import * as object from '#/utilities/object'
Expand All @@ -8,10 +10,36 @@ import * as object from '#/utilities/object'
// ====================

/** Metadata describing runtime behavior associated with a {@link LocalStorageKey}. */
export interface LocalStorageKeyMetadata<K extends LocalStorageKey> {
export type LocalStorageKeyMetadata<K extends LocalStorageKey> =
| LocalStorageKeyMetadataWithParseFunction<K>
| LocalStorageKeyMetadataWithSchema<K>

/**
* A {@link LocalStorageKeyMetadata} with a `tryParse` function.
*/
interface LocalStorageKeyMetadataWithParseFunction<K extends LocalStorageKey> {
readonly isUserSpecific?: boolean
/** A type-safe way to deserialize a value from `localStorage`. */
/**
* A function to parse a value from the stored data.
* If this is provided, the value will be parsed using this function.
* If this is not provided, the value will be parsed using the `schema`.
*/
readonly tryParse: (value: unknown) => LocalStorageData[K] | null
readonly schema?: never
}

/**
* A {@link LocalStorageKeyMetadata} with a `schema`.
*/
interface LocalStorageKeyMetadataWithSchema<K extends LocalStorageKey> {
readonly isUserSpecific?: boolean
/**
* The Zod schema to validate the value.
* If this is provided, the value will be parsed using this schema.
* If this is not provided, the value will be parsed using the `tryParse` function.
*/
readonly schema: z.ZodType<LocalStorageData[K]>
readonly tryParse?: never
}

/** The data that can be stored in a {@link LocalStorage}.
Expand All @@ -38,8 +66,11 @@ export default class LocalStorage {
for (const [key, metadata] of object.unsafeEntries(LocalStorage.keyMetadata)) {
if (key in savedValues) {
// This is SAFE, as it is guarded by the `key in savedValues` check.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-restricted-syntax, @typescript-eslint/no-explicit-any
const value = metadata.tryParse((savedValues as any)[key])
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-restricted-syntax, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const savedValue = (savedValues as any)[key]
const value = metadata.schema
? metadata.schema.safeParse(savedValue).data
: metadata.tryParse(savedValue)
if (value != null) {
// This is SAFE, as the `tryParse` function is required by definition to
// return a value of the correct type.
Expand Down

0 comments on commit 9331e69

Please sign in to comment.