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 { ...@@ -43,13 +43,15 @@ export function useAuth(): Auth {
*/ */
export default function AuthProvider({ export default function AuthProvider({
children, children,
onSignIn, onChange,
onSignOut,
}: PropsWithChildren<{ }: PropsWithChildren<{
onSignIn?: () => void onChange?: () => void
onSignOut?: () => void
}>): JSX.Element { }>): JSX.Element {
const [session, setSession] = useLocalStorage<Session | null>('session', null) const [session, setSession] = useLocalStorage<Session | null>(
'session',
null,
onChange,
)
const auth = useMemo(() => { const auth = useMemo(() => {
function signIn(token: string) { function signIn(token: string) {
...@@ -62,13 +64,11 @@ export default function AuthProvider({ ...@@ -62,13 +64,11 @@ export default function AuthProvider({
accessToken: token, accessToken: token,
expiresAt: decoded.exp, expiresAt: decoded.exp,
}) })
onSignIn?.()
} }
} }
function signOut() { function signOut() {
setSession(null) setSession(null)
onSignOut?.()
} }
function validateToken(token: string) { function validateToken(token: string) {
......
...@@ -25,14 +25,6 @@ import AuthProvider from '@/modules/auth/AuthContext' ...@@ -25,14 +25,6 @@ import AuthProvider from '@/modules/auth/AuthContext'
import ClientError from '@/modules/error/ClientError' import ClientError from '@/modules/error/ClientError'
import PageLayout from '@/modules/page/PageLayout' 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. * Report page transitions to Matomo analytics.
*/ */
...@@ -72,8 +64,15 @@ function createQueryClient() { ...@@ -72,8 +64,15 @@ function createQueryClient() {
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
cacheTime: Infinity, // cacheTime: Infinity,
staleTime: 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, structuralSharing: false,
}, },
}, },
...@@ -85,21 +84,30 @@ function createQueryClient() { ...@@ -85,21 +84,30 @@ function createQueryClient() {
/** /**
* Providers. * Providers.
*/ */
function Providers({ children }: PropsWithChildren<unknown>) { function Providers({
children,
render,
}: PropsWithChildren<{ render: () => void }>) {
const [queryClient] = useState(() => createQueryClient()) const [queryClient] = useState(() => createQueryClient())
useInteractionModality() useInteractionModality()
const [clearQueryCache] = useState(() => {
return () => {
queryClient.clear()
/**
* Clearing the query cache means removing all query subscribers.
* Rerendering the tree registers them again.
*/
render()
}
})
return ( return (
<SSRProvider> <SSRProvider>
<I18nProvider locale="en"> <I18nProvider locale="en">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthProvider <AuthProvider onChange={clearQueryCache}>{children}</AuthProvider>
onSignIn={queryClient.clear}
onSignOut={queryClient.clear}
>
{children}
</AuthProvider>
</QueryClientProvider> </QueryClientProvider>
</I18nProvider> </I18nProvider>
</SSRProvider> </SSRProvider>
...@@ -114,13 +122,16 @@ export default function App({ ...@@ -114,13 +122,16 @@ export default function App({
pageProps, pageProps,
router, router,
}: AppProps): JSX.Element { }: AppProps): JSX.Element {
// eslint-disable-next-line @typescript-eslint/ban-types
const [, forceRender] = useState<object>({})
return ( return (
<Fragment> <Fragment>
<Head> <Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head> </Head>
<ErrorBoundary fallback={ClientError} resetOnChange={[router.asPath]}> <ErrorBoundary fallback={ClientError} resetOnChange={[router.asPath]}>
<Providers {...pageProps}> <Providers {...pageProps} render={() => forceRender({})}>
<Layout {...pageProps} default={PageLayout}> <Layout {...pageProps} default={PageLayout}>
<Component {...pageProps} /> <Component {...pageProps} />
</Layout> </Layout>
......
...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types' ...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { DatasetDto } from '@/api/sshoc' import type { DatasetDto } from '@/api/sshoc'
import { useGetDataset } from '@/api/sshoc' import { useGetDataset } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/dataset/[id]/index' import type { PageProps } from '@/pages/dataset/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout' import ItemLayout from '@/screens/item/ItemLayout'
...@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout' ...@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout'
export default function DatasetScreen({ export default function DatasetScreen({
dataset: initialData, dataset: initialData,
}: PageProps): JSX.Element { }: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/** /**
* populate client cache with data from getServerSideProps, * populate client cache with data from getServerSideProps,
* to allow background refreshing * to allow background refreshing
...@@ -21,6 +25,7 @@ export default function DatasetScreen({ ...@@ -21,6 +25,7 @@ export default function DatasetScreen({
{ id: initialData.persistentId! }, { id: initialData.persistentId! },
{}, {},
{ enabled: initialData.persistentId !== undefined, initialData }, { enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
) )
/** backend does not specify required fields. should be safe here */ /** backend does not specify required fields. should be safe here */
const dataset = (data ?? initialData) as DeepRequired<DatasetDto> const dataset = (data ?? initialData) as DeepRequired<DatasetDto>
......
...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types' ...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { PublicationDto } from '@/api/sshoc' import type { PublicationDto } from '@/api/sshoc'
import { useGetPublication } from '@/api/sshoc' import { useGetPublication } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/publication/[id]/index' import type { PageProps } from '@/pages/publication/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout' import ItemLayout from '@/screens/item/ItemLayout'
...@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout' ...@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout'
export default function PublicationScreen({ export default function PublicationScreen({
publication: initialData, publication: initialData,
}: PageProps): JSX.Element { }: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/** /**
* populate client cache with data from getServerSideProps, * populate client cache with data from getServerSideProps,
* to allow background refreshing * to allow background refreshing
...@@ -21,6 +25,7 @@ export default function PublicationScreen({ ...@@ -21,6 +25,7 @@ export default function PublicationScreen({
{ id: initialData.persistentId! }, { id: initialData.persistentId! },
{}, {},
{ enabled: initialData.persistentId !== undefined, initialData }, { enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
) )
/** backend does not specify required fields. should be safe here */ /** backend does not specify required fields. should be safe here */
const publication = (data ?? initialData) as DeepRequired<PublicationDto> const publication = (data ?? initialData) as DeepRequired<PublicationDto>
......
...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types' ...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { ToolDto } from '@/api/sshoc' import type { ToolDto } from '@/api/sshoc'
import { useGetTool } 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 type { PageProps } from '@/pages/tool-or-service/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout' import ItemLayout from '@/screens/item/ItemLayout'
...@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout' ...@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout'
export default function ToolScreen({ export default function ToolScreen({
tool: initialData, tool: initialData,
}: PageProps): JSX.Element { }: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/** /**
* populate client cache with data from getServerSideProps, * populate client cache with data from getServerSideProps,
* to allow background refreshing * to allow background refreshing
...@@ -21,6 +25,7 @@ export default function ToolScreen({ ...@@ -21,6 +25,7 @@ export default function ToolScreen({
{ id: initialData.persistentId! }, { id: initialData.persistentId! },
{}, {},
{ enabled: initialData.persistentId !== undefined, initialData }, { enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
) )
/** backend does not specify required fields. should be safe here */ /** backend does not specify required fields. should be safe here */
const tool = (data ?? initialData) as DeepRequired<ToolDto> const tool = (data ?? initialData) as DeepRequired<ToolDto>
......
...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types' ...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { TrainingMaterialDto } from '@/api/sshoc' import type { TrainingMaterialDto } from '@/api/sshoc'
import { useGetTrainingMaterial } from '@/api/sshoc' import { useGetTrainingMaterial } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/training-material/[id]/index' import type { PageProps } from '@/pages/training-material/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout' import ItemLayout from '@/screens/item/ItemLayout'
...@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout' ...@@ -13,6 +14,9 @@ import ItemLayout from '@/screens/item/ItemLayout'
export default function TrainingMaterialScreen({ export default function TrainingMaterialScreen({
trainingMaterial: initialData, trainingMaterial: initialData,
}: PageProps): JSX.Element { }: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/** /**
* populate client cache with data from getServerSideProps, * populate client cache with data from getServerSideProps,
* to allow background refreshing * to allow background refreshing
...@@ -21,6 +25,7 @@ export default function TrainingMaterialScreen({ ...@@ -21,6 +25,7 @@ export default function TrainingMaterialScreen({
{ id: initialData.persistentId! }, { id: initialData.persistentId! },
{}, {},
{ enabled: initialData.persistentId !== undefined, initialData }, { enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
) )
/** backend does not specify required fields. should be safe here */ /** backend does not specify required fields. should be safe here */
const trainingMaterial = (data ?? const trainingMaterial = (data ??
......
...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types' ...@@ -4,6 +4,7 @@ import type { DeepRequired } from 'utility-types'
import type { WorkflowDto } from '@/api/sshoc' import type { WorkflowDto } from '@/api/sshoc'
import { useGetWorkflow } from '@/api/sshoc' import { useGetWorkflow } from '@/api/sshoc'
import { useAuth } from '@/modules/auth/AuthContext'
import type { PageProps } from '@/pages/workflow/[id]/index' import type { PageProps } from '@/pages/workflow/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout' import ItemLayout from '@/screens/item/ItemLayout'
import Steps from '@/screens/item/workflow/Steps' import Steps from '@/screens/item/workflow/Steps'
...@@ -14,6 +15,9 @@ import Steps from '@/screens/item/workflow/Steps' ...@@ -14,6 +15,9 @@ import Steps from '@/screens/item/workflow/Steps'
export default function WorkflowScreen({ export default function WorkflowScreen({
workflow: initialData, workflow: initialData,
}: PageProps): JSX.Element { }: PageProps): JSX.Element {
/** token is used to get hidden properties */
const auth = useAuth()
/** /**
* populate client cache with data from getServerSideProps, * populate client cache with data from getServerSideProps,
* to allow background refreshing * to allow background refreshing
...@@ -22,6 +26,7 @@ export default function WorkflowScreen({ ...@@ -22,6 +26,7 @@ export default function WorkflowScreen({
{ workflowId: initialData.persistentId! }, { workflowId: initialData.persistentId! },
{}, {},
{ enabled: initialData.persistentId !== undefined, initialData }, { enabled: initialData.persistentId !== undefined, initialData },
{ token: auth.session?.accessToken },
) )
/** backend does not specify required fields. should be safe here */ /** backend does not specify required fields. should be safe here */
const workflow = (data ?? initialData) as DeepRequired<WorkflowDto> 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 */ import type { JSON } from '@/utils/ts/json'
type Json =
| null
| boolean
| string
| number
| Array<Json>
| { [key: string]: Json }
type ValueOrSetter<T> = T | ((previousValue: T) => T) 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, key: string,
initialValue: T, initialValue: T,
onChange?: () => void,
): [T, (value: ValueOrSetter<T>) => void] { ): [T, (value: ValueOrSetter<T>) => void] {
const [storedValue, setStoredValue] = useState<T>(() => { const [storedValue, _setStoredValue] = useState<T>(initialValue)
if (typeof window === 'undefined') {
return initialValue
}
try { const setStoredValue = useCallback(
const item = window.localStorage.getItem(key) function setStoredValue(value: ValueOrSetter<T>) {
if (item === null) return initialValue _setStoredValue(value)
return JSON.parse(item) onChange?.()
} catch (error) { },
console.error(error) [onChange],
return initialValue )
useEffect(() => {
const item = getItem(key)
if (item !== undefined) {
setStoredValue(item)
} }
}) }, [key, setStoredValue])
const setValue = useCallback( const setValue = useCallback(
function setValue(value: ValueOrSetter<T>) { function setValue(value: ValueOrSetter<T>) {
try { setStoredValue((previousValue) => {
/** mirror useState api */
const newValue = const newValue =
typeof value === 'function' ? value(storedValue) : value typeof value === 'function' ? value(previousValue) : value
window.localStorage.setItem(key, JSON.stringify(newValue))
setItem(key, newValue)
setStoredValue(newValue) setStoredValue(newValue)
} catch (error) {
console.error(error) return newValue
} })
}, },
[key, storedValue], [key, setStoredValue],
) )
return [storedValue, setValue] 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: ...@@ -10334,10 +10334,10 @@ react-is@^17.0.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
   
react-query@^3.12.0: react-query@^3.18.1:
version "3.12.0" version "3.18.1"
resolved "https://registry.npmjs.org/react-query/-/react-query-3.12.0.tgz#a2082a167f3e394e84dfd3cec0f8c7503abf33dc" resolved "https://registry.npmjs.org/react-query/-/react-query-3.18.1.tgz#893b5475a7b4add099e007105317446f7a2cd310"
integrity sha512-WJYECeZ6xT2oxIlgqXUjLNLWRvJbeelXscVnAFfyUFgO21OYEYHMWPG61V9W57EUUqrXioQsNPsU9XyddfEvXQ== integrity sha512-17lv3pQxU9n+cB5acUv0/cxNTjo9q8G+RsedC6Ax4V9D8xEM7Q5xf9xAbCPdEhDrrnzPjTls9fQEABKRSi7OJA==
dependencies: dependencies:
"@babel/runtime" "^7.5.5" "@babel/runtime" "^7.5.5"
broadcast-channel "^3.4.1" 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