Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add corpus import ID builder for superusers to CorpusForm #139

Merged
merged 9 commits into from
Jan 7, 2025
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
133 changes: 85 additions & 48 deletions src/components/forms/CorpusForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
ButtonGroup,
FormErrorMessage,
useToast,
FormHelperText,
Tooltip,
Icon,
ModalOverlay,
Expand All @@ -46,6 +45,9 @@ import { WYSIWYG } from '../form-components/WYSIWYG'
import * as yup from 'yup'
import { stripHtml } from '@/utils/stripHtml'
import { convertEmptyToNull } from '@/utils/convertEmptyToNull'
import { TextField } from './fields/TextField'
import { ImportIdSection } from './sections/ImportIdSection'
import { FormLoader } from '../feedback/FormLoader'

interface CorpusType {
name: string
Expand All @@ -56,7 +58,7 @@ type TProps = {
corpus?: ICorpus
}

type CorpusFormData = yup.InferType<typeof corpusSchema>
export type CorpusFormData = yup.InferType<typeof corpusSchema>

export const CorpusForm = ({ corpus: loadedCorpus }: TProps) => {
const navigate = useNavigate()
Expand All @@ -73,6 +75,13 @@ export const CorpusForm = ({ corpus: loadedCorpus }: TProps) => {
watch,
} = useForm<CorpusFormData>({
resolver: yupResolver(corpusSchema),
context: {
isNewCorpus: loadedCorpus ? false : true,
},
defaultValues: {
import_id_part2: 'corpus',
import_id_part4: 'n0000',
},
})
const { config, loading: configLoading, error: configError } = useConfig()

Expand Down Expand Up @@ -137,6 +146,7 @@ export const CorpusForm = ({ corpus: loadedCorpus }: TProps) => {
}

const formData: ICorpusFormPost = {
import_id: `${formValues.import_id_part1.value}.${formValues.import_id_part2}.${formValues.import_id_part3}.${formValues.import_id_part4}`,
title: formValues.title,
description: formValues.description,
corpus_text: convertEmptyToNull(
Expand Down Expand Up @@ -248,12 +258,12 @@ export const CorpusForm = ({ corpus: loadedCorpus }: TProps) => {
[uniqueCorpusTypes, setValue, isDescriptionManuallyEdited],
)

const getOrganisationNameById = useCallback(
const getOrganisationDisplayNameById = useCallback(
(organisationId: number): string | undefined => {
const organisation = config?.corpora?.find(
(corpus) => corpus.organisation?.id === organisationId,
)
return organisation?.organisation?.name
return organisation?.organisation?.display_name
},
[config?.corpora],
)
Expand All @@ -271,8 +281,11 @@ export const CorpusForm = ({ corpus: loadedCorpus }: TProps) => {

useEffect(() => {
if (loadedCorpus && !configLoading) {
const orgName = getOrganisationNameById(loadedCorpus.organisation_id)
const orgName = getOrganisationDisplayNameById(
loadedCorpus.organisation_id,
)
reset({
import_id: loadedCorpus.import_id || '',
title: loadedCorpus.title || '',
description: loadedCorpus.description || '',
corpus_text: loadedCorpus.corpus_text || '',
Expand All @@ -294,7 +307,7 @@ export const CorpusForm = ({ corpus: loadedCorpus }: TProps) => {
updateCorpusTypeDescription(loadedCorpus.corpus_type_name)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadedCorpus, configLoading, getOrganisationNameById, reset])
}, [loadedCorpus, configLoading, getOrganisationDisplayNameById, reset])

const corpusTextOnChange = (html: string) => {
if (stripHtml(html) === '') {
Expand All @@ -303,21 +316,29 @@ export const CorpusForm = ({ corpus: loadedCorpus }: TProps) => {
setValue('corpus_text', html, { shouldDirty: true })
}

const watchedOrganisation = watch('organisation_id')
const watchedImportIdPart1 = watch('import_id_part1')

return (
<>
{configError && <ApiError error={configError} />}
{configLoading && <FormLoader />}

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

{loadedCorpus && (
<FormControl isRequired isReadOnly isDisabled>
<FormLabel>Import ID</FormLabel>
<Input bg='white' value={loadedCorpus?.import_id} />
<FormHelperText>You cannot edit this</FormHelperText>
</FormControl>
<TextField
name='import_id'
label='Import ID'
control={control}
isRequired={true}
showHelperText={true}
isDisabled={true}
/>
)}

<FormControl isRequired>
<FormLabel>Title</FormLabel>
<Input bg='white' {...register('title')} />
Expand Down Expand Up @@ -403,43 +424,59 @@ export const CorpusForm = ({ corpus: loadedCorpus }: TProps) => {
</FormControl>
)}

<Controller
control={control}
name='organisation_id'
render={({ field }) => (
<FormControl
as='fieldset'
isRequired
isInvalid={!!errors.organisation_id}
>
<FormLabel as='legend'>Organisation</FormLabel>
<div data-testid='organisation-select'>
<CRSelect
chakraStyles={chakraStylesSelect}
isClearable={true}
isMulti={false}
isSearchable={true}
options={Array.from(
new Set(
config?.corpora?.map((corpus) => ({
label: corpus.organisation?.name,
value: corpus.organisation?.id,
})),
),
).map((org) => ({
label: org.label,
value: org.value,
}))}
isDisabled={!!loadedCorpus}
{...field}
/>
</div>
<FormErrorMessage>
{errors.organisation_id?.message}
</FormErrorMessage>
</FormControl>
)}
/>
{config && !configLoading && (
<Controller
control={control}
name='organisation_id'
render={({ field }) => (
<FormControl
as='fieldset'
isRequired
isInvalid={!!errors.organisation_id}
>
<FormLabel as='legend'>Organisation</FormLabel>
<div data-testid='organisation-select'>
<CRSelect
chakraStyles={chakraStylesSelect}
isClearable={true}
isMulti={false}
isSearchable={true}
options={Array.from(
new Set(
config?.corpora?.map(
(corpus) => corpus.organisation?.id,
),
),
).map((id) => {
const corpus = config?.corpora?.find(
(corpus) => corpus.organisation?.id === id,
)
return {
label: corpus?.organisation?.display_name,
value: id,
}
})}
isDisabled={!!loadedCorpus}
{...field}
/>
</div>
<FormErrorMessage>
{errors.organisation_id?.message}
</FormErrorMessage>
</FormControl>
)}
/>
)}

{!loadedCorpus && config && !configLoading && (
<ImportIdSection
corpora={config?.corpora || []}
watchedOrganisation={watchedOrganisation}
watchedImportIdPart1={watchedImportIdPart1}
control={control}
setValue={setValue}
/>
)}

<Modal isOpen={isModalOpen} onClose={handleModalCancel}>
<ModalOverlay />
Expand Down
15 changes: 14 additions & 1 deletion src/components/forms/fields/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
FormControl,
FormLabel,
FormErrorMessage,
FormHelperText,
} from '@chakra-ui/react'

type TProps<T extends FieldValues> = {
Expand All @@ -13,6 +14,8 @@ type TProps<T extends FieldValues> = {
placeholder?: string
label?: string
isRequired?: boolean
showHelperText?: boolean
isDisabled?: boolean
}

export const TextField = <T extends FieldValues>({
Expand All @@ -22,21 +25,31 @@ export const TextField = <T extends FieldValues>({
placeholder,
label,
isRequired,
showHelperText,
isDisabled,
}: TProps<T>) => {
return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl isInvalid={!!error} isRequired={isRequired}>
<FormControl
isInvalid={!!error}
isRequired={isRequired}
isReadOnly={isDisabled}
isDisabled={isDisabled}
>
{label && <FormLabel>{label}</FormLabel>}
<Input
{...field} // This destructured object contains the value
bg='white'
type={type}
placeholder={placeholder ?? `Enter ${name}`}
/>
{showHelperText && isDisabled && (
<FormHelperText>You cannot edit this</FormHelperText>
)}
{error && <FormErrorMessage>{error.message}</FormErrorMessage>}
</FormControl>
)
Expand Down
125 changes: 125 additions & 0 deletions src/components/forms/sections/ImportIdSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Box, Divider, AbsoluteCenter, Text } from '@chakra-ui/react'
import { IChakraSelect, IConfigCorpora } from '@/interfaces'
import { SelectField } from '../fields/SelectField'
import { TextField } from '../fields/TextField'
import { Control, UseFormSetValue } from 'react-hook-form'
import { useEffect, useState } from 'react'
import { CorpusFormData } from '../CorpusForm'

type TProps = {
corpora: IConfigCorpora[]
watchedOrganisation: { label?: string; value?: number }
watchedImportIdPart1: { label?: string; value?: string }
control: Control<CorpusFormData>
setValue: UseFormSetValue<CorpusFormData>
}

export const ImportIdSection = ({
corpora,
watchedOrganisation,
watchedImportIdPart1,
control,
setValue,
}: TProps) => {
const [selectOptions, setSelectOptions] = useState<IChakraSelect[]>([])

useEffect(() => {
if (watchedOrganisation) {
// Find the corresponding corpus based on the watched organisation ID
const matchedCorpus = corpora?.find(
(corpus) => corpus.organisation?.id === watchedOrganisation?.value,
)

// If a matched corpus is found, create the options based on its name and type
const filteredOptions: IChakraSelect[] = matchedCorpus
? [
{
label: matchedCorpus.organisation?.name,
value: matchedCorpus.organisation?.name,
},
{
label: matchedCorpus.organisation?.type,
value: matchedCorpus.organisation?.type,
},
]
: [] // Empty array if no matched corpus is found

setSelectOptions(filteredOptions)

// Clear the value of import_id_part1 when organisation changes
setValue('import_id_part1', {})
} else {
setValue('import_id_part1', {})
setSelectOptions([])
}
}, [watchedOrganisation, corpora, setValue])

useEffect(() => {
if (watchedOrganisation && watchedImportIdPart1) {
// Find the corresponding corpus based on the watched organisation ID
const matchedCorpus = corpora?.find(
(corpus) => corpus.organisation?.id === watchedOrganisation?.value,
)

if (watchedImportIdPart1?.value === matchedCorpus?.organisation?.type)
setValue('import_id_part3', matchedCorpus?.organisation?.name)
else setValue('import_id_part3', 'i00000001')
} else setValue('import_id_part3', 'i00000001')
}, [watchedOrganisation, watchedImportIdPart1, corpora, setValue])

return (
<>
<Box position='relative' padding='10'>
<Divider />
<AbsoluteCenter bg='gray.50' px='4'>
Import ID Builder
</AbsoluteCenter>
</Box>

{!watchedOrganisation && (
<Text>
Please select an organisation first before attempting to build an
import ID
</Text>
)}

<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<SelectField
name='import_id_part1'
label='Part 1'
control={control}
options={selectOptions}
isRequired={true}
/>

<span style={{ margin: '0 8px' }}>·</span>

<TextField
name='import_id_part2'
label='Part 2'
control={control}
showHelperText={false}
isDisabled={true}
isRequired={true}
/>
<span style={{ margin: '0 8px' }}>·</span>

<TextField
name='import_id_part3'
label='Part 3'
control={control}
isRequired={true}
/>

<span style={{ margin: '0 8px' }}>·</span>

<TextField
name='import_id_part4'
label='Part 4'
control={control}
isRequired={true}
/>
</div>
</>
)
}
Loading
Loading