Skip to content

Commit

Permalink
Feature/app 13 allow users to add multiple authors to reports (#152)
Browse files Browse the repository at this point in the history
Co-authored-by: Anna Pokorska <[email protected]>
Co-authored-by: Osneil Drakes <[email protected]>
  • Loading branch information
3 people authored Jan 27, 2025
1 parent 7cc869c commit 6e4514e
Show file tree
Hide file tree
Showing 18 changed files with 1,234 additions and 1,401 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@types/draftjs-to-html": "^0.8.3",
"@types/html-to-draftjs": "^1.4.2",
"@types/jwt-decode": "^3.1.0",
"@types/jwt-encode": "^1.0.3",
"@types/node": "^22.5.5",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
Expand Down
12 changes: 11 additions & 1 deletion src/components/forms/DynamicMetadataFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { formatFieldLabel } from '@/utils/metadataUtils'
import { SelectField } from './fields/SelectField'
import { TextField } from './fields/TextField'
import { ITaxonomyField, TSubTaxonomy } from '@/interfaces'
import { MultiValueInput } from './fields/MultiValueInput'

type TProps<T extends FieldValues> = {
fieldKey: string
Expand Down Expand Up @@ -43,7 +44,7 @@ export const DynamicMetadataFields = <T extends FieldValues>({
const isRequired = !allow_blanks

const renderField = () => {
if (allow_any) {
if (allow_any && fieldType !== FieldType.MULTI_VALUE_INPUT) {
return (
<TextField<T>
name={fieldKey as Path<T>}
Expand Down Expand Up @@ -78,6 +79,15 @@ export const DynamicMetadataFields = <T extends FieldValues>({
label={formatFieldLabel(fieldKey)}
/>
)
case FieldType.MULTI_VALUE_INPUT:
return (
<MultiValueInput
name={fieldKey as Path<T>}
control={control}
label={formatFieldLabel(fieldKey)}
isRequired={isRequired}
/>
)
case FieldType.TEXT:
default:
return (
Expand Down
6 changes: 4 additions & 2 deletions src/components/forms/FamilyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => {
label: 'Intergovernmental Organization',
}

setValue('author', corpusAuthor)
setValue('author_type', corpusAuthorType)
setValue('author', [corpusAuthor])
setValue('author_type', [corpusAuthorType])
}
}, [watchCorpus, isMCFCorpus, loadedFamily, setValue])

Expand Down Expand Up @@ -312,6 +312,8 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => {
value: v,
label: v,
}))
} else if (fieldConfig.type === FieldType.MULTI_VALUE_INPUT) {
loadedMetadata[key] = value
} else {
loadedMetadata[key] = value?.[0]
}
Expand Down
117 changes: 117 additions & 0 deletions src/components/forms/fields/MultiValueInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useState } from 'react'
import {
Box,
Input,
Tag,
TagLabel,
TagCloseButton,
VStack,
HStack,
FormLabel,
FormControl,
FormHelperText,
FormErrorMessage,
} from '@chakra-ui/react'
import { Controller, Control, FieldValues, Path } from 'react-hook-form'

type TProps<T extends FieldValues> = {
name: Path<T>
control: Control<T>
type?: 'text' | 'number'
label?: string
isRequired?: boolean
showHelperText?: boolean
isDisabled?: boolean
}

export const MultiValueInput = <T extends FieldValues>({
name,
control,
type = 'text',
label,
showHelperText,
isDisabled,
}: TProps<T>) => {
const [inputValue, setInputValue] = useState('')

return (
<Controller
name={name}
control={control}
render={({ field, fieldState: { error } }) => {
const handleAddValue = () => {
const currentValues = (field.value as string[]) || []
if (inputValue.trim() && !currentValues.includes(inputValue.trim())) {
const newValues = [...currentValues, inputValue.trim()]
field.onChange(newValues)
setInputValue('')
}
}

const handleRemoveValue = (valueToRemove: string) => {
const currentValues = field.value || []
const newValues = currentValues.filter(
(value) => value !== valueToRemove,
)
field.onChange(newValues)
}

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddValue()
}
}

return (
<FormControl
isInvalid={!!error}
isReadOnly={isDisabled}
isDisabled={isDisabled}
isRequired={field.value?.length === 0}
>
{label && <FormLabel>{label}</FormLabel>}
<FormHelperText mb={2}>
You are able to add multiple values
</FormHelperText>
<Box>
<VStack spacing={4} align='stretch'>
<HStack>
<Input
{...field} // This destructured object contains the value
name={name}
type={type}
bg='white'
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='Type a value and press Enter'
value={inputValue}
/>
</HStack>
<HStack wrap='wrap' spacing={2}>
{(field.value || []).map((value: string, index: number) => (
<Tag
key={index}
size='lg'
colorScheme='gray'
borderRadius='full'
>
<TagLabel>{value}</TagLabel>
<TagCloseButton
onClick={() => handleRemoveValue(value)}
/>
</Tag>
))}
</HStack>
</VStack>
</Box>
{showHelperText && isDisabled && (
<FormHelperText>You cannot edit this</FormHelperText>
)}
{error && <FormErrorMessage>{error.message}</FormErrorMessage>}
</FormControl>
)
}}
/>
)
}
11 changes: 5 additions & 6 deletions src/components/forms/metadata-handlers/familyForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ interface IFamilyFormGefProjects extends IFamilyFormMcfProjects {
}

interface IFamilyFormReports extends IFamilyFormBase {
author?: string
author_type?: IChakraSelect
author?: string[]
author_type?: IChakraSelect[]
external_id?: string
}

Expand All @@ -89,6 +89,7 @@ export type TFamilyFormSubmit =
| IFamilyFormLawsAndPolicies
| IFamilyFormIntlAgreements
| TFamilyFormMcfProjects
| IFamilyFormReports

// Mapping of corpus types to their specific metadata handlers
export const corpusMetadataHandlers: Record<
Expand Down Expand Up @@ -249,10 +250,8 @@ export const corpusMetadataHandlers: Record<
extractMetadata: (formData: TFamilyFormSubmit) => {
const reportsData = formData as IFamilyFormReports
return {
author: reportsData.author ? [reportsData.author] : [],
author_type: reportsData.author_type
? [reportsData.author_type?.value]
: [],
author: reportsData.author ? reportsData.author : [],
author_type: reportsData.author_type?.map((type) => type.value),
external_id: reportsData.external_id ? [reportsData.external_id] : [],
} as IReportsMetadata
},
Expand Down
13 changes: 13 additions & 0 deletions src/interfaces/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export interface IDefaultDocSubTaxonomy extends ISubTaxonomy {
type: ITaxonomyField
}

export interface IReportsDocSubTaxonomy extends ISubTaxonomy {
type: ITaxonomyField
}

export interface IGcfDocSubTaxonomy extends ISubTaxonomy {
type: ITaxonomyField
}
Expand Down Expand Up @@ -64,6 +68,14 @@ export interface IConfigTaxonomyUNFCCC extends ITaxonomy {
_event: IEventSubTaxonomy
}

export interface IConfigReportsTaxonomy extends ITaxonomy {
author: ITaxonomyField
author_type: ITaxonomyField
event_type: ITaxonomyField
_document: IReportsDocSubTaxonomy
_event: IEventSubTaxonomy
}

interface IConfigMCFBaseTaxonomy extends ITaxonomy {
event_type: ITaxonomyField
implementing_agency: ITaxonomyField
Expand Down Expand Up @@ -108,6 +120,7 @@ export type TTaxonomy =
| IConfigTaxonomyCCLW
| IConfigTaxonomyUNFCCC
| IConfigTaxonomyMCF
| IConfigReportsTaxonomy

// Config endpoint types.
export interface IConfigGeographyNode {
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/Family.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type TFamilyMetadata =
| IInternationalAgreementsMetadata
| ILawsAndPoliciesMetadata
| TMcfProjectsMetadata
| IReportsMetadata

// Read DTOs.
interface IFamilyBase {
Expand Down Expand Up @@ -174,3 +175,4 @@ export type TFamilyFormPost =
| ILawsAndPoliciesFamilyFormPost
| IInternationalAgreementsFamilyFormPost
| TMcfFamilyFormPost
| IReportsFamilyFormPost
5 changes: 3 additions & 2 deletions src/interfaces/Metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum FieldType {
SINGLE_SELECT = 'single_select',
NUMBER = 'number',
DATE = 'date',
MULTI_VALUE_INPUT = 'multi_value_input',
}

export interface IMetadata {
Expand Down Expand Up @@ -170,8 +171,8 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = {
},
Reports: {
renderFields: {
author: { type: FieldType.TEXT },
author_type: { type: FieldType.SINGLE_SELECT },
author: { type: FieldType.MULTI_VALUE_INPUT },
author_type: { type: FieldType.MULTI_SELECT },
external_id: { type: FieldType.TEXT },
},
validationFields: ['author', 'author_type', 'external_id'],
Expand Down
7 changes: 7 additions & 0 deletions src/schemas/dynamicValidationSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ const getFieldValidation = (
}),
)
break
case FieldType.MULTI_VALUE_INPUT:
fieldValidation = yup
.array()
.of(yup.string())
.required()
.min(1, 'You must provide at least one author')
break
case FieldType.SINGLE_SELECT:
fieldValidation = yup.object({
value: yup.string().required(),
Expand Down
26 changes: 26 additions & 0 deletions src/tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import sign from 'jwt-encode'

export const setupUser = (
organisationName: string = 'CPR',
email: string = '[email protected]',
isSuperuser: boolean = true,
isAdmin: boolean = true,
orgId: number = 1,
) => {
localStorage.setItem(
'token',
sign(
{
email: email,
is_superuser: isSuperuser,
authorisation: {
[organisationName]: {
is_admin: isAdmin,
},
org_id: orgId,
},
},
'',
),
)
}
16 changes: 16 additions & 0 deletions src/tests/mocks/api/configHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { cclwConfigMock, mcfConfigMock } from '@/tests/utilsTest/mocks'
import { http, HttpResponse } from 'msw'
import { jwtDecode } from 'jwt-decode'

export const configHandlers = [
http.get('*/v1/config', (req) => {
const authHeader = req.request.headers.get('authorization')
const parsedAuthToken: Record<string, object> = jwtDecode(authHeader || '')
const authorisation = parsedAuthToken?.authorisation || {}
const org = Object.keys(authorisation)[0]
if (org && ['GCF', 'GEF', 'AF', 'CIF'].includes(org)) {
return HttpResponse.json({ ...mcfConfigMock })
}
return HttpResponse.json({ ...cclwConfigMock })
}),
]
7 changes: 2 additions & 5 deletions src/tests/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { http, HttpResponse } from 'msw'
import { cclwConfigMock } from '../utilsTest/mocks'
import { familyHandlers } from './api/familyHandlers'
import { collectionHandlers } from './api/collectionHandlers'
import { eventHandlers } from './api/eventHandlers'
import { documentHandlers } from './api/documentHandlers'
import { configHandlers } from './api/configHandlers'

export const handlers = [
http.get('*/v1/config', () => {
return HttpResponse.json({ ...cclwConfigMock })
}),
...configHandlers,
...familyHandlers,
...collectionHandlers,
...eventHandlers,
Expand Down
4 changes: 2 additions & 2 deletions src/tests/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { cleanup } from '@testing-library/react'
import { reset } from './mocks/repository.ts'
import { server } from './mocks/server.ts'
import * as matchers from '@testing-library/jest-dom/matchers'
import sign from 'jwt-encode'
import { vi } from 'vitest'
import { setupUser } from './helpers.ts'

expect.extend(matchers)

Expand Down Expand Up @@ -60,7 +60,7 @@ window.IntersectionObserver =
// Establish API mocking before all tests.
beforeAll(() => {
server.listen()
localStorage.setItem('token', sign({ is_superuser: true }, ''))
setupUser()
})

// Reset any request handlers that we may add during the tests,
Expand Down
2 changes: 1 addition & 1 deletion src/tests/utils/modifyConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IConfig } from '@/interfaces'
import { modifyConfig } from '@utils/modifyConfig'

describe('modifyConfig', () => {
it(' adds a languagesSorted property to the config object with languages sorted alphabetically by label', () => {
it('adds a languagesSorted property to the config object with languages sorted alphabetically by label', () => {
const mockConfig: IConfig = {
document: {
roles: ['role1', 'role2'],
Expand Down
Loading

0 comments on commit 6e4514e

Please sign in to comment.