Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions frontend/src/api/auth/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { restAPI } from '../baseRestApi';
import { invalidateEntity, queryKeys } from '@/api/queryKeys';
import { queryClient } from '@/api/queryClient';
import { clearTokenFromCookie } from '@/api/utils';

export type Token = string | undefined;

Expand All @@ -22,8 +23,9 @@ export const AuthRestAPI = restAPI.injectEndpoints({
}),
logout: builder.mutation<null, void>({
queryFn: async () => {
document.cookie = 'token=;max-age=0;path=/';
queryClient.invalidateQueries()
clearTokenFromCookie()
queryClient.invalidateQueries({ queryKey: queryKeys.user.current })
// .then(() => queryClient.clear())
return { data: null }
},
}),
Expand All @@ -33,8 +35,8 @@ export const AuthRestAPI = restAPI.injectEndpoints({
headers: {
'Content-Type': 'text/markdown',
},
responseHandler: 'text'
})
responseHandler: 'text',
}),
}),
}),
})
6 changes: 3 additions & 3 deletions frontend/src/api/graphqlClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RequestExtendedOptions } from 'graphql-request';
import { GraphQLClient } from 'graphql-request';
import { getTokenFromCookie } from '@/api/utils';
import { clearTokenFromCookie, getTokenFromCookie } from '@/api/utils';


/**
Expand All @@ -10,7 +10,7 @@ const requestMiddleware = async (request: any) => {
const token = getTokenFromCookie();

if (!token) {
document.cookie = 'token=;max-age=0;path=/';
clearTokenFromCookie()
throw new Error('Unauthorized');
}

Expand All @@ -35,7 +35,7 @@ const responseMiddleware = (response: any) => {
);

if (hasAuthError) {
document.cookie = 'token=;max-age=0;path=/';
clearTokenFromCookie()
throw new Error('Unauthorized');
}

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export function getTokenFromCookie(): Token | undefined {
const tokenCookie = document.cookie?.split(';').filter((item) => item.trim().startsWith('token='))[0];
return tokenCookie?.split('=').pop();
}
export function clearTokenFromCookie(): void {
document.cookie = 'token=;max-age=0;path=/';
}

export function prepareHeaders(headers: Headers) {
// Set Authorization
Expand Down
42 changes: 21 additions & 21 deletions frontend/src/features/AnnotationPhase/PhaseTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import { addOutline, closeOutline } from 'ionicons/icons/index.js';
import { Button, Tab, useAlert, useModal } from '@/components/ui';
import { AnnotationPhaseType } from '@/api';
import { AnnotationPhaseCreateAnnotationModal, AnnotationPhaseCreateVerificationModal } from './PhaseCreateModal'
import { useLoaderData, useParams } from '@tanstack/react-router';
import { useMutation } from '@tanstack/react-query';
import { useParams } from '@tanstack/react-router';
import { useMutation, useQuery } from '@tanstack/react-query';
import { endMutation } from './api'
import { queryClient } from '@/api/queryClient';
import { queryKeys } from '@/api/queryKeys';
import { AnnotationCampaign } from '@/features';

export const AnnotationPhaseTab: React.FC<{ phaseType: AnnotationPhaseType }> = ({ phaseType: phaseType }) => {
const { phaseType: currentPhaseType } = useParams({ strict: false });
const { campaign, phases } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID' })
const phase = useMemo(() => phases?.find(p => p.phase === phaseType), [ phases, phaseType ])
const { campaignID } = useParams({ from: '/_authenticated/annotation-campaign/$campaignID' })
const { data, isFetching } = useQuery(AnnotationCampaign.API.byIdQuery({ id: campaignID }))
const phase = useMemo(() => data?.phases?.find(p => p.phase === phaseType), [ data, phaseType ])

const alert = useAlert();
const verificationModal = useModal(AnnotationPhaseCreateVerificationModal);
Expand All @@ -27,8 +29,8 @@ export const AnnotationPhaseTab: React.FC<{ phaseType: AnnotationPhaseType }> =
annotationModal.toggle()
break;
case AnnotationPhaseType.Verification:
if (!phases) return;
if (phases.find(p => p.phase === 'Annotation')) return verificationModal.toggle()
if (!data) return;
if (data.phases.find(p => p.phase === 'Annotation')) return verificationModal.toggle()
else {
return alert.showAlert({
type: 'Warning',
Expand All @@ -42,18 +44,13 @@ export const AnnotationPhaseTab: React.FC<{ phaseType: AnnotationPhaseType }> =
})
}
}
}, [ phases, annotationModal, verificationModal, alert, phaseType ])
}, [ data, annotationModal, verificationModal, alert, phaseType ])

const onSuccess = useCallback(() => {
queryClient.invalidateQueries({ queryKey: queryKeys.campaign.byId({ id: campaign.id }) })
queryClient.invalidateQueries({ queryKey: queryKeys.campaign.byId({ id: campaignID }) })
queryClient.invalidateQueries({ queryKey: queryKeys.campaign.base })
queryClient.invalidateQueries({
queryKey: queryKeys.phase.get({
campaignID: campaign.id,
phase: phaseType,
}),
})
}, [ campaign, phaseType ])
queryClient.invalidateQueries({ queryKey: queryKeys.phase.get({ campaignID, phase: phaseType }) })
}, [ campaignID, phaseType ])
const { mutate: endPhase } = useMutation({
...endMutation,
onSuccess,
Expand All @@ -71,20 +68,23 @@ export const AnnotationPhaseTab: React.FC<{ phaseType: AnnotationPhaseType }> =
} ],
});
} else endPhase({ id: phase.id })
}, [ endPhase, phase, campaign, alert ]);
}, [ endPhase, phase, alert ]);

if (phase)
if (data?.campaign && phase)
return <Tab to="/annotation-campaign/$campaignID/phase/$phaseType"
params={ { campaignID: campaign.id, phaseType } } active={ currentPhaseType === phaseType }>
disabled={isFetching}
params={ { campaignID, phaseType } } active={ currentPhaseType === phaseType }>
{ phaseType }

{ campaign.isEditable && campaign.isUserAllowedToManage && currentPhaseType === phaseType && phase?.isOpen &&
{ data.campaign.isEditable && data.campaign.isUserAllowedToManage && currentPhaseType === phaseType && phase?.isOpen &&
<IonIcon icon={ closeOutline } slot="end" onClick={ end }/> }
</Tab>
if (!campaign.isEditable || !campaign.isUserAllowedToManage) return <Fragment/>
if (!data?.campaign?.isEditable || !data?.campaign?.isUserAllowedToManage) return <Fragment/>

return <Fragment>
<Button fill="clear" color="medium" onClick={ openModal }>
<Button fill="clear" color="medium"
disabled={isFetching}
onClick={ openModal }>
{ phaseType }
<IonIcon icon={ addOutline } slot="end"/>
</Button>
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/features/User/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import { cleanGqlList } from '@/api/utils';
export const currentQuery = queryOptions({
queryKey: queryKeys.user.current,
queryFn: () => graphqlClient.request<GetCurrentUserQuery>(GetCurrentUserDocument, {})
.then(data => data.currentUser!),
.then(data => data.currentUser!)
.catch(e => {
if (e.message === 'Unauthorized') return null
throw e
}),
})

export const allQuery = queryOptions({
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/routes/(public)/login.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { IonButton, IonSpinner } from '@ionic/react';
import { useQuery } from '@tanstack/react-query';

import { Link, useToast } from '@/components/ui';
import { Input } from '@/components/form';
import { getErrorMessage } from '@/service/function';

import { useLogin } from '@/api';
import { NON_FILTERED_KEY_DOWN_EVENT, useEvent } from '@/features/UX';
import { User } from '@/features'

import styles from './public.module.scss';

Expand All @@ -25,6 +27,7 @@ const Login: React.FC = () => {
const to = useMemo(() => search?.redirect || '/annotation-campaign', [ search ]);
const [ login, { isLoading, error: loginError } ] = useLogin();
const toast = useToast()
const { refetch: refetchUser } = useQuery(User.API.currentQuery)

useEffect(() => {
return () => {
Expand All @@ -43,11 +46,10 @@ const Login: React.FC = () => {
if (!username || !password) return;

await login({ username, password }).unwrap()
.then(() => {
navigate({ to, replace: true })
})
.then(() => refetchUser())
.then(() => navigate({ to, replace: true }))
.catch(error => setErrors({ global: getErrorMessage(error) }));
}, [ setErrors, username, password, navigate, to, login ])
}, [ setErrors, username, password, navigate, to, login, refetchUser ])

const onKbdEvent = useCallback((event: KeyboardEvent) => {
switch (event.code) {
Expand Down
14 changes: 5 additions & 9 deletions frontend/src/routes/_authenticated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { queryKeys } from '@/api/queryKeys';
import { User } from '@/features';

const Component: React.FC = () => {
const { status, error, isFetching } = useQuery(User.API.currentQuery)
const { status, error, isFetching, data: user } = useQuery(User.API.currentQuery)

const navigate = useNavigate();
const router = useRouter();
Expand All @@ -29,19 +29,15 @@ const Component: React.FC = () => {
}, [ router, toast, navigate, error ])

useEffect(() => {
if (!isFetching && status === 'error') handleNotConnected()
}, [ status ]);
if (!isFetching && (status === 'error' || !user)) handleNotConnected()
}, [ status, user ]);

return <AploseSkeleton><Outlet/></AploseSkeleton>
}
export const Route = createFileRoute('/_authenticated')({
loader: async () => {
try {
const user = await queryClient.ensureQueryData(User.API.currentQuery)
if (user) return { user }
} catch (e) {
if (![ 'Unauthorized', 'Authentication failed' ].includes((e as Error).message)) throw e
}
const user = await queryClient.ensureQueryData(User.API.currentQuery)
if (user) return { user }
throw redirect({
to: '/login',
search: { redirect: location.pathname.replace('/app', '') },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ import styles from './phase.$phaseType.module.scss';
import { queryClient } from '@/api/queryClient';
import { AnnotationPhase, AnnotationSpectrogram, User } from '@/features';
import { IonNote, IonSpinner } from '@ionic/react';
import { useQuery } from '@tanstack/react-query';

const AnnotationCampaignPhaseDetail: React.FC = () => {
const { campaign, phases } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID' })
const { phase, spectrograms, spectrogramsPageCount } = Route.useLoaderData()
const { spectrograms, spectrogramsPageCount } = Route.useLoaderData()
const { campaignID, phaseType } = Route.useParams()
const { data: phase } = useQuery(AnnotationPhase.API.getQuery({
campaignID,
phase: phaseType,
}))

const search = Route.useSearch();
const routeParams = Route.useParams()
Expand Down Expand Up @@ -57,7 +63,7 @@ const AnnotationCampaignPhaseDetail: React.FC = () => {

<FileRangeActionBar/>

{ phase.phase === 'Verification' && !phase.hasAnnotations && phases.find(p => p.phase === AnnotationPhaseType.Verification) &&
{ phase?.phase === 'Verification' && !phase?.hasAnnotations && phases.find(p => p.phase === AnnotationPhaseType.Verification) &&
<WarningText message="Your campaign doesn't have any annotations to check"
children={ <ImportAnnotationsButton/> }/> }

Expand All @@ -74,9 +80,9 @@ const AnnotationCampaignPhaseDetail: React.FC = () => {
<Th scope="col" center filterable
isFiltered={ search.withAnnotations ?? false }
onFilterClick={ annotationFilterModal.open }>
Annotations{ phase.phase === 'Verification' && <Fragment><br/>to check</Fragment> }
Annotations{ phase?.phase === 'Verification' && <Fragment><br/>to check</Fragment> }
</Th>
{ phase.phase === 'Verification' && <Th scope="col" center>Validated<br/>annotations</Th> }
{ phase?.phase === 'Verification' && <Th scope="col" center>Validated<br/>annotations</Th> }
<Th scope="col" center filterable
isFiltered={ search.status !== undefined || search.onlyAssigned !== undefined }
onFilterClick={ statusFilterModal.open }>
Expand Down Expand Up @@ -136,7 +142,7 @@ export const Route = createFileRoute('/_authenticated/annotation-campaign/$campa
queryClient.ensureQueryData(AnnotationSpectrogram.API.allQuery({
campaignID,
phaseType,
annotatorID: user.id,
annotatorID: user!.id,
limit: PAGE_SIZE,
offset: PAGE_SIZE * ((deps.page ?? 1) - 1),
...deps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import {
} from '@/features/AnnotationCampaign';
import { queryClient } from '@/api/queryClient';
import { AnnotationCampaign } from '@/features';
import { useQuery } from '@tanstack/react-query';

const AnnotationCampaignList: React.FC = () => {
const navigate = useNavigate();
const { user } = useLoaderData({ from: '/_authenticated' })
const campaigns = Route.useLoaderData()
const params = Route.useParams()
const search = Route.useSearch()
const { data: campaigns } = useQuery(AnnotationCampaign.API.allQuery({ ...search, ...params }))

const init = useCallback(() => {
navigate({
Expand Down
Loading