Commit 21f1e553 authored by Stefan Probst's avatar Stefan Probst
Browse files

fix: show hidden properties, correctly clear cache on sign-in

parent cb8f2e19
Pipeline #209692 passed with stages
in 9 minutes and 45 seconds
......@@ -43,13 +43,15 @@ export function useAuth(): Auth {
*/
export default function AuthProvider({
children,
onSignIn,
onSignOut,
onChange,
}: PropsWithChildren<{
onSignIn?: () => void
onSignOut?: () => void
onChange?: () => void
}>): JSX.Element {
const [session, setSession] = useLocalStorage<Session | null>('session', null)
const [session, setSession] = useLocalStorage<Session | null>(
'session',
null,
onChange,
)
const auth = useMemo(() => {
function signIn(token: string) {
......@@ -62,13 +64,11 @@ export default function AuthProvider({
accessToken: token,
expiresAt: decoded.exp,
})
onSignIn?.()
}
}
function signOut() {
setSession(null)
onSignOut?.()
}
function validateToken(token: string) {
......
......@@ -25,14 +25,6 @@ import AuthProvider from '@/modules/auth/AuthContext'
import ClientError from '@/modules/error/ClientError'
import PageLayout from '@/modules/page/PageLayout'
/**
* Report web vitals.
*/
export function reportWebVitals(metric: NextWebVitalsMetric): void {
/** should be dispatched to an analytics service */
// console.info(metric)
}
/**
* Report page transitions to Matomo analytics.
*/
......@@ -72,8 +64,15 @@ function createQueryClient() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
cacheTime: Infinity,
staleTime: Infinity,
// cacheTime: Infinity,
/**
* stale time must not be set to infinite, because this will interfere
* with refetching after clearing the query cache when a user signs in/out.
*
* TODO: is this because the cache gets reset to its initialData (which is
* unauthenticated data fetched server-side)?
*/
// staleTime: Infinity,
structuralSharing: false,
},
},
......@@ -85,21 +84,30 @@ function createQueryClient() {
/**
* Providers.
*/
function Providers({ children }: PropsWithChildren<unknown>) {
function Providers({
children,
render,
}: PropsWithChildren<{ render: () => void }>) {
const [queryClient] = useState(() => createQueryClient())
useInteractionModality()
const [clearQueryCache] = useState(() => {
return () => {
queryClient.clear()
/**
* Clearing the query cache means removing all query subscribers.
* Rerendering the tree registers them again.
*/
render()
}
})
return (
<SSRProvider>
<I18nProvider locale="en">
<QueryClientProvider client={queryClient}>
<AuthProvider
onSignIn={queryClient.clear}
onSignOut={queryClient.clear}
>
{children}
</AuthProvider>
<AuthProvider onChange={clearQueryCache}>{children}</AuthProvider>
</QueryClientProvider>
</I18nProvider>
</SSRProvider>
......@@ -114,13 +122,16 @@ export default function App({
pageProps,
router,
}: AppProps): JSX.Element {
// eslint-disable-next-line @typescript-eslint/ban-types
const [, forceRender] = useState<object>({})
return (
<Fragment>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<ErrorBoundary fallback={ClientError} resetOnChange={[router.asPath]}>
<Providers {...pageProps}>
<Providers {...pageProps} render={() => forceRender({})}>
<Layout {...pageProps} default={PageLayout}>
<Component {...pageProps} />
</Layout>
......
......@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { DatasetDto } from '@/api/sshoc'
import { useGetDataset } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/dataset/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout'
......@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout'
export default function DatasetScreen({
dataset: initialData,
}: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/**
* populate client cache with data from getServerSideProps,
* to allow background refreshing
......@@ -21,6 +25,7 @@ export default function DatasetScreen({
{ id: initialData.persistentId! },
{},
{ enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
)
/** backend does not specify required fields. should be safe here */
const dataset = (data ?? initialData) as DeepRequired<DatasetDto>
......
......@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { PublicationDto } from '@/api/sshoc'
import { useGetPublication } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/publication/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout'
......@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout'
export default function PublicationScreen({
publication: initialData,
}: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/**
* populate client cache with data from getServerSideProps,
* to allow background refreshing
......@@ -21,6 +25,7 @@ export default function PublicationScreen({
{ id: initialData.persistentId! },
{},
{ enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
)
/** backend does not specify required fields. should be safe here */
const publication = (data ?? initialData) as DeepRequired<PublicationDto>
......
......@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { ToolDto } from '@/api/sshoc'
import { useGetTool } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/tool-or-service/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout'
......@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout'
export default function ToolScreen({
tool: initialData,
}: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/**
* populate client cache with data from getServerSideProps,
* to allow background refreshing
......@@ -21,6 +25,7 @@ export default function ToolScreen({
{ id: initialData.persistentId! },
{},
{ enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
)
/** backend does not specify required fields. should be safe here */
const tool = (data ?? initialData) as DeepRequired<ToolDto>
......
......@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { TrainingMaterialDto } from '@/api/sshoc'
import { useGetTrainingMaterial } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/training-material/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout'
......@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout'
export default function TrainingMaterialScreen({
trainingMaterial: initialData,
}: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/**
* populate client cache with data from getServerSideProps,
* to allow background refreshing
......@@ -21,6 +25,7 @@ export default function TrainingMaterialScreen({
{ id: initialData.persistentId! },
{},
{ enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
)
/** backend does not specify required fields. should be safe here */
const trainingMaterial = (data ??
......
......@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { WorkflowDto } from '@/api/sshoc'
import { useGetWorkflow } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/workflow/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout'
import Steps from '@/screens/item/workflow/Steps'
......@@ -14,6 +15,9 @@ import Steps from '@/screens/item/workflow/Steps'
export default function WorkflowScreen({
workflow: initialData,
}: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/**
* populate client cache with data from getServerSideProps,
* to allow background refreshing
......@@ -22,6 +26,7 @@ export default function WorkflowScreen({
{ workflowId: initialData.persistentId! },
{},
{ enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
)
/** backend does not specify required fields. should be safe here */
const workflow = (data ?? initialData) as DeepRequired<WorkflowDto>
......
/** JSON serializable */
export type JSON =
| null
| boolean
| string
| number
| Array<JSON>
| { [key: string]: JSON }
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
/** JSON serializable */
type Json =
| null
| boolean
| string
| number
| Array<Json>
| { [key: string]: Json }
import type { JSON } from '@/utils/ts/json'
type ValueOrSetter<T> = T | ((previousValue: T) => T)
export function useLocalStorage<T extends Json>(
/**
* Synchronizes state to local storage.
*/
export function useLocalStorage<T extends JSON>(
key: string,
initialValue: T,
onChange?: () => void,
): [T, (value: ValueOrSetter<T>) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
const [storedValue, _setStoredValue] = useState<T>(initialValue)
const setStoredValue = useCallback(
function setStoredValue(value: ValueOrSetter<T>) {
_setStoredValue(value)
onChange?.()
},
[onChange],
)
useEffect(() => {
const item = getItem(key)
try {
const item = window.localStorage.getItem(key)
if (item === null) return initialValue
return JSON.parse(item)
} catch (error) {
console.error(error)
return initialValue
if (item !== undefined) {
setStoredValue(item)
}
})
}, [key, setStoredValue])
const setValue = useCallback(
function setValue(value: ValueOrSetter<T>) {
try {
/** mirror useState api */
setStoredValue((previousValue) => {
const newValue =
typeof value === 'function' ? value(storedValue) : value
window.localStorage.setItem(key, JSON.stringify(newValue))
typeof value === 'function' ? value(previousValue) : value
setItem(key, newValue)
setStoredValue(newValue)
} catch (error) {
console.error(error)
}
return newValue
})
},
[key, storedValue],
[key, setStoredValue],
)
return [storedValue, setValue]
}
/**
* Retrieves and parses value from local storage.
*/
function getItem(key: string) {
try {
const item = window.localStorage.getItem(key)
if (item === null) return undefined
return JSON.parse(item)
} catch {
return undefined
}
}
/**
* Saves value to local storage.
*/
function setItem(key: string, value: unknown) {
try {
if (value == null) {
window.localStorage.removeItem(key)
} else {
const item = JSON.stringify(value)
window.localStorage.setItem(key, item)
}
} catch {
/** Dont't set anything when local storage not supported. */
}
}
......@@ -10334,10 +10334,10 @@ react-is@^17.0.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
 
react-query@^3.12.0:
version "3.12.0"
resolved "https://registry.npmjs.org/react-query/-/react-query-3.12.0.tgz#a2082a167f3e394e84dfd3cec0f8c7503abf33dc"
integrity sha512-WJYECeZ6xT2oxIlgqXUjLNLWRvJbeelXscVnAFfyUFgO21OYEYHMWPG61V9W57EUUqrXioQsNPsU9XyddfEvXQ==
react-query@^3.18.1:
version "3.18.1"
resolved "https://registry.npmjs.org/react-query/-/react-query-3.18.1.tgz#893b5475a7b4add099e007105317446f7a2cd310"
integrity sha512-17lv3pQxU9n+cB5acUv0/cxNTjo9q8G+RsedC6Ax4V9D8xEM7Q5xf9xAbCPdEhDrrnzPjTls9fQEABKRSi7OJA==
dependencies:
"@babel/runtime" "^7.5.5"
broadcast-channel "^3.4.1"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment