Skip to content

Commit

Permalink
Feature/app 55 read corpus type information via admin service UI (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
katybaulch authored Feb 6, 2025
1 parent bb39ddc commit 6e1ab8b
Show file tree
Hide file tree
Showing 12 changed files with 709 additions and 3 deletions.
4 changes: 2 additions & 2 deletions src/api/CorpusTypes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AxiosError } from 'axios'

import API from '@/api'
import { IError } from '@/interfaces'
import { setToken } from '@/api/Auth'
import { IError } from '@/interfaces'
import { ICorpusType } from '@/interfaces/CorpusType'

export async function getCorpusTypes() {
Expand All @@ -17,7 +17,7 @@ export async function getCorpusTypes() {
status: error.response?.status || 500,
detail: error.response?.data?.detail || 'Unknown error',
message: error.message,
returnPage: '/corpora',
returnPage: '/corpus-types',
}
throw e
})
Expand Down
11 changes: 11 additions & 0 deletions src/components/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ export function SideMenu() {
</IconLink>
</>
)}
{isSuperUser && (
<>
<IconLink
icon={<Icon as={GoLog} mr='2' />}
to='/corpus-types'
current={isCurrentPage('corpus-types')}
>
Corpus Types
</IconLink>
</>
)}
<IconLink icon={<Icon as={GoClock} mr='2' />}>
View audit history
</IconLink>
Expand Down
205 changes: 205 additions & 0 deletions src/components/forms/CorpusTypeForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { useEffect, useRef, useState, useCallback } from 'react'
import { useForm, SubmitHandler, SubmitErrorHandler } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { IError } from '@/interfaces'
import { corpusTypeSchema } from '@/schemas/corpusTypeSchema'
import {
FormControl,
FormLabel,
Textarea,
VStack,
Button,
ButtonGroup,
useToast,
Tooltip,
Icon,
ModalOverlay,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalContent,
ModalFooter,
Modal,
} from '@chakra-ui/react'
import { ApiError } from '../feedback/ApiError'
import { InfoOutlineIcon } from '@chakra-ui/icons'
import { TextField } from './fields/TextField'
import * as Yup from 'yup'
import { ICorpusType } from '@/interfaces/CorpusType'

type TProps = {
corpusType?: ICorpusType
}

export type ICorpusTypeFormSubmit = Yup.InferType<typeof corpusTypeSchema>

export const CorpusTypeForm = ({ corpusType: loadedCorpusType }: TProps) => {
const toast = useToast()
const [formError, setFormError] = useState<IError | null | undefined>()
const {
register,
handleSubmit,
control,
reset,
formState: { isSubmitting },
getValues,
} = useForm<ICorpusTypeFormSubmit>({
resolver: yupResolver(corpusTypeSchema),
})

const initialDescription = useRef<string | undefined>(
loadedCorpusType?.description,
)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isConfirmed, setIsConfirmed] = useState(false)

const handleFormSubmission = useCallback(
// TODO: Remove under APP-54.
/* trunk-ignore(eslint/@typescript-eslint/require-await) */
async (formValues: ICorpusTypeFormSubmit) => {
setFormError(null)

// Only check for corpus type description changes if updating an existing corpus
if (
loadedCorpusType &&
formValues.description !== initialDescription.current &&
!isConfirmed
) {
setIsModalOpen(true)
return
}

if (loadedCorpusType) {
toast({
title: 'Not implemented',
description: 'Corpus type update has not been implemented',
status: 'error',
position: 'top',
})
} else {
toast({
title: 'Not implemented',
description: 'Corpus type update has not been implemented',
status: 'error',
position: 'top',
})
}
},
[loadedCorpusType, isConfirmed, initialDescription, toast, setFormError],
)

const onSubmit: SubmitHandler<ICorpusTypeFormSubmit> = useCallback(
(data) => {
handleFormSubmission(data).catch((error: IError) => {
console.error(error)
})
},
[handleFormSubmission],
)

const onSubmitErrorHandler: SubmitErrorHandler<ICorpusTypeFormSubmit> =
useCallback((errors) => {
console.error(errors)
}, [])

const handleModalConfirm = () => {
setIsConfirmed(true)
setIsModalOpen(false)
}

const handleModalCancel = () => {
setIsModalOpen(false)
}

const handleFormSubmissionWithConfirmation = useCallback(() => {
if (isConfirmed) {
void handleSubmit(onSubmit, onSubmitErrorHandler)().catch((error) => {
console.error('Form submission error:', error)
})
}
}, [isConfirmed, handleSubmit, onSubmit, onSubmitErrorHandler])

useEffect(() => {
handleFormSubmissionWithConfirmation()
}, [handleFormSubmissionWithConfirmation])

useEffect(() => {
if (loadedCorpusType) {
reset({
name: loadedCorpusType?.name || '',
description: loadedCorpusType?.description || '',
})
}
}, [loadedCorpusType, reset])

return (
<>
<form onSubmit={handleSubmit(onSubmit, onSubmitErrorHandler)}>
<VStack gap='4' mb={12} align={'stretch'}>
{formError && <ApiError error={formError} />}

<TextField
name='name'
label='Title'
control={control}
isRequired={true}
/>

<FormControl isRequired>
<FormLabel>
Description
<Tooltip label='Updating this will also apply this change to all other corpora of this type'>
<Icon as={InfoOutlineIcon} ml={2} cursor='pointer' />
</Tooltip>
</FormLabel>
<Textarea
height={'100px'}
bg='white'
{...register('description')}
/>
</FormControl>

<Modal isOpen={isModalOpen} onClose={handleModalCancel}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Confirm Update</ModalHeader>
<ModalCloseButton />
<ModalBody data-testid='modal-body'>
<p>
You have changed the corpus type description of{' '}
<strong>{getValues('name') || 'unknown'}</strong>.
</p>
<br></br>
<p>
This will update all corpora with the type{' '}
<strong>{getValues('name') || 'unknown'}</strong> with the
description{' '}
<em style={{ color: 'blue' }}>
{getValues('description') || 'unknown'}
</em>
.
</p>
<br></br>
Do you wish to proceed?
</ModalBody>
<ModalFooter>
<Button colorScheme='blue' mr={3} onClick={handleModalConfirm}>
Confirm
</Button>
<Button variant='ghost' onClick={handleModalCancel}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
</Modal>

<ButtonGroup>
<Button type='submit' colorScheme='blue' disabled={isSubmitting}>
{(loadedCorpusType ? 'Update ' : 'Create new ') + 'Corpus type'}
</Button>
</ButtonGroup>
</VStack>
</form>
</>
)
}
146 changes: 146 additions & 0 deletions src/components/lists/CorpusTypeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { IError } from '@/interfaces'
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
IconButton,
Box,
HStack,
Tooltip,
SkeletonText,
} from '@chakra-ui/react'
import { GoPencil } from 'react-icons/go'

import { Loader } from '../Loader'
import { sortBy } from '@/utils/sortBy'
import { ArrowDownIcon, ArrowUpIcon, ArrowUpDownIcon } from '@chakra-ui/icons'
import { ApiError } from '../feedback/ApiError'
import { ICorpusType } from '@/interfaces/CorpusType'
import useCorpusTypes from '@/hooks/useCorpusTypes'

export default function CorpusTypeList() {
const [sortControls, setSortControls] = useState<{
key: keyof ICorpusType
reverse: boolean
}>({ key: 'name', reverse: false })
const [filteredItems, setFilteredItems] = useState<ICorpusType[]>()
const { corpusTypes, loading, error } = useCorpusTypes()
const [corpusError] = useState<string | null | undefined>()
const [formError] = useState<IError | null | undefined>()

const renderSortIcon = (key: keyof ICorpusType) => {
if (sortControls.key !== key) {
return <ArrowUpDownIcon />
}
if (sortControls.reverse) {
return <ArrowDownIcon />
} else {
return <ArrowUpIcon />
}
}

const handleHeaderClick = (key: keyof ICorpusType) => {
if (sortControls.key === key) {
setSortControls({ key, reverse: !sortControls.reverse })
} else {
setSortControls({ key, reverse: false })
}
}

useEffect(() => {
if (corpusTypes) {
const sortedItems = corpusTypes
.slice()
.sort(sortBy(sortControls.key, sortControls.reverse))
setFilteredItems(sortedItems)
} else {
setFilteredItems([])
}
}, [sortControls, corpusTypes])

useEffect(() => {
setFilteredItems(corpusTypes)
}, [corpusTypes])

return (
<>
{loading && (
<Box padding='4' bg='white'>
<Loader />
<SkeletonText mt='4' noOfLines={3} spacing='4' skeletonHeight='2' />
</Box>
)}
{!loading && (
<Box flex={1}>
<Box>
{error && <ApiError error={error} />}
{formError && <ApiError error={formError} />}
</Box>
<TableContainer height={'100%'} whiteSpace={'normal'}>
<Table size='sm' variant={'striped'}>
<Thead>
<Tr>
<Th
onClick={() => handleHeaderClick('name')}
cursor='pointer'
>
Name {renderSortIcon('name')}
</Th>
<Th
onClick={() => handleHeaderClick('description')}
cursor='pointer'
>
Description {renderSortIcon('description')}
</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{filteredItems?.length === 0 && (
<Tr>
<Td colSpan={4}>
No results found, please amend your search
</Td>
</Tr>
)}
{filteredItems?.map((corpus) => (
<Tr
key={corpus.name}
borderLeft={corpus.name === corpusError ? '2px' : 'inherit'}
borderColor={
corpus.name === corpusError ? 'red.500' : 'inherit'
}
>
<Td>{corpus.name}</Td>
<Td>{corpus.description}</Td>
<Td>
<HStack gap={2}>
<Tooltip label='Edit'>
<Link to={`/corpus-type/${corpus.name}/edit`}>
<IconButton
aria-label='Edit corpus type'
icon={<GoPencil />}
variant='outline'
size='sm'
colorScheme='blue'
/>
</Link>
</Tooltip>
</HStack>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
)}
</>
)
}
Loading

0 comments on commit 6e1ab8b

Please sign in to comment.