-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/app 55 read corpus type information via admin service UI (#165)
- Loading branch information
1 parent
bb39ddc
commit 6e1ab8b
Showing
12 changed files
with
709 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
)} | ||
</> | ||
) | ||
} |
Oops, something went wrong.