From b1af0a0af0f6e09e4c23f2bd0923401826faae9d Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:38:07 +0000 Subject: [PATCH 01/88] Add dynamic metadata field rendering --- src/components/forms/FamilyForm.tsx | 452 ++++++++++++++++------------ 1 file changed, 264 insertions(+), 188 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 401a99d..a8a0624 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useMemo, useCallback } from 'react' import { useForm, SubmitHandler, Controller } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { useBlocker, useNavigate } from 'react-router-dom' - +import * as yup from 'yup' import { IError, TFamilyFormPost, @@ -14,8 +14,6 @@ import { IEvent, ICollection, IConfigCorpus, - IConfigTaxonomyUNFCCC, - IConfigTaxonomyCCLW, IDecodedToken, } from '@/interfaces' @@ -109,6 +107,45 @@ const getCollection = (collectionId: string, collections: ICollection[]) => { return collections.find((collection) => collection.import_id === collectionId) } +// Define a type for corpus metadata configuration +type CorpusMetadataConfig = { + [corpusType: string]: { + renderFields: string[] + validationFields: string[] + } +} + +// Centralized configuration for corpus metadata +const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { + 'Intl. agreements': { + renderFields: ['author', 'author_type'], + validationFields: ['author', 'author_type'], + }, + 'Laws and Policies': { + renderFields: [ + 'topic', + 'hazard', + 'sector', + 'keyword', + 'framework', + 'instrument', + ], + validationFields: [ + 'topic', + 'hazard', + 'sector', + 'keyword', + 'framework', + 'instrument', + ], + }, + // Easy to extend for new corpus types + default: { + renderFields: [], + validationFields: [], + }, +} + export const FamilyForm = ({ family: loadedFamily }: TProps) => { const [isLeavingModalOpen, setIsLeavingModalOpen] = useState(false) const [isFormSubmitting, setIsFormSubmitting] = useState(false) @@ -130,6 +167,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { reset, setError, setValue, + getValues, formState: { errors, isSubmitting }, formState: { dirtyFields }, } = useForm({ @@ -152,11 +190,15 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { watchCorpus?.value, ) + console.log('config?.corpora', config?.corpora) + const corpusTitle = loadedFamily ? loadedFamily?.corpus_title : corpusInfo?.title const taxonomy = useTaxonomy(corpusInfo?.corpus_type, corpusInfo?.taxonomy) + console.log('corpusInfo', corpusInfo) + console.log('taxonomy', taxonomy) const userToken = useMemo(() => { const token = localStorage.getItem('token') @@ -446,6 +488,224 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { } }, [handleBeforeUnload]) + const renderDynamicMetadataFields = useCallback(() => { + if (!corpusInfo || !taxonomy) return null + + // Get render fields based on corpus type, fallback to default + const { renderFields } = + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type] || + CORPUS_METADATA_CONFIG['default'] + + return renderFields + .map((fieldKey) => { + // Check if the field exists in the taxonomy + const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] + if (!taxonomyField) return null + + // Determine field rendering based on taxonomy configuration + const allowedValues = taxonomyField.allowed_values || [] + const isAllowAny = taxonomyField.allow_any + const isAllowBlanks = taxonomyField.allow_blanks + + // Render free text input if allow_any is true + if (isAllowAny) { + return ( + + + {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} + + ( + + )} + /> + {errors[fieldKey] && ( + + {errors[fieldKey]?.message as string} + + )} + + ) + } + + // Special handling for author_type (radio group) + if (fieldKey === 'author_type') { + return ( + + Author Type + ( + + + {allowedValues.map((value) => ( + + {value} + + ))} + + + )} + /> + {errors[fieldKey] && ( + + Please select an author type + + )} + + ) + } + + // Render select box if allowed_values is not empty + if (allowedValues.length > 0) { + return ( + + + {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} + + ( + + )} + /> + {errors[fieldKey] && ( + + {errors[fieldKey]?.message as string} + + )} + {fieldKey !== 'author' && ( + + You can search and select multiple options + + )} + + ) + } + + // Fallback to default text input if no specific rendering rules apply + return ( + + + {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} + + ( + + )} + /> + {errors[fieldKey] && ( + + {errors[fieldKey]?.message as string} + + )} + + ) + }) + .filter(Boolean) + }, [corpusInfo, taxonomy, control, errors]) + + const dynamicValidationSchema = useMemo(() => { + if (!taxonomy) return familySchema + + // Get validation fields based on corpus type, fallback to default + const { validationFields } = + CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] || + CORPUS_METADATA_CONFIG['default'] + + const metadataValidation = validationFields.reduce((acc, fieldKey) => { + const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] + + if (taxonomyField) { + // Get allowed values for the current field + const allowedValues = taxonomyField.allowed_values || [] + + // If allow_any is true, use a simple string validation + if (taxonomyField.allow_any) { + acc[fieldKey] = yup.string() + } + // For multi-select fields with allowed values + else if ( + allowedValues.length > 0 && + fieldKey !== 'author' && + fieldKey !== 'author_type' + ) { + acc[fieldKey] = yup.array().of(yup.string()) + } + // For single select or text fields + else { + // Use allow_blanks to determine if the field is required + acc[fieldKey] = taxonomyField.allow_blanks + ? yup.string() + : yup.string().required(`${fieldKey} is required`) + } + } + + return acc + }, {} as any) + + return familySchema.shape({ + ...metadataValidation, + }) + }, [taxonomy, corpusInfo, familySchema]) + + useEffect(() => { + if (taxonomy) { + // Get fields to reset based on corpus type + const { validationFields } = + CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] || + CORPUS_METADATA_CONFIG['default'] + + const currentValues = getValues() + const resetValues = validationFields.reduce((acc, field) => { + acc[field] = undefined + return acc + }, {} as any) + + reset( + { + ...currentValues, + ...resetValues, + }, + { + keepErrors: false, + keepDirty: true, + }, + ) + + // Update validation resolver + setValue('resolver', yupResolver(dynamicValidationSchema)) + } + }, [taxonomy, corpusInfo, dynamicValidationSchema]) + return ( <> {(configLoading || collectionsLoading) && ( @@ -669,191 +929,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { )} - {corpusInfo !== null && - corpusInfo?.corpus_type === 'Intl. agreements' && ( - <> - - Author - - - { - const tax = taxonomy as IConfigTaxonomyUNFCCC - return ( - - Author type - - - {tax?.author_type.allowed_values.map( - (authorType) => ( - - {authorType} - - ), - )} - - - - Please select an author type - - - ) - }} - /> - - )} - {corpusInfo !== null && - corpusInfo?.corpus_type === 'Laws and Policies' && ( - <> - { - const tax = taxonomy as IConfigTaxonomyCCLW - return ( - - Topics - - - - You are able to search and can select multiple - options. - - - ) - }} - /> - { - const tax = taxonomy as IConfigTaxonomyCCLW - return ( - - Hazards - - - ) - }} - /> - { - const tax = taxonomy as IConfigTaxonomyCCLW - return ( - - Sectors - - - ) - }} - /> - { - const tax = taxonomy as IConfigTaxonomyCCLW - return ( - - Keywords - - - ) - }} - /> - { - const tax = taxonomy as IConfigTaxonomyCCLW - return ( - - Frameworks - - - ) - }} - /> - { - const tax = taxonomy as IConfigTaxonomyCCLW - return ( - - Instruments - - - ) - }} - /> - - )} + {renderDynamicMetadataFields()} From bd7a91d7adb3b5aff23cae0c14028119c8932a72 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:02:16 +0000 Subject: [PATCH 02/88] Move dynamic metadata field rendering to new file --- .../forms/DynamicMetadataFields.tsx | 260 ++++++++++++++++++ src/components/forms/FamilyForm.tsx | 257 ++--------------- 2 files changed, 280 insertions(+), 237 deletions(-) create mode 100644 src/components/forms/DynamicMetadataFields.tsx diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx new file mode 100644 index 0000000..2e4908d --- /dev/null +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -0,0 +1,260 @@ +import React from 'react' +import { + FormControl, + FormLabel, + FormErrorMessage, + FormHelperText, + Input, + RadioGroup, + Radio, + HStack, + Box, + AbsoluteCenter, + Divider +} from '@chakra-ui/react' +import { Controller, Control, FieldErrors } from 'react-hook-form' +import { Select as CRSelect } from 'chakra-react-select' +import * as yup from 'yup' + +// Utility function to generate select options +export const generateOptions = (values: string[]) => + values.map(value => ({ value, label: value })) + +// Configuration type for corpus metadata +export type CorpusMetadataConfig = { + [corpusType: string]: { + renderFields: string[] + validationFields: string[] + } +} + +// Centralized configuration for corpus metadata +export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { + 'Intl. agreements': { + renderFields: ['author', 'author_type'], + validationFields: ['author', 'author_type'], + }, + 'Laws and Policies': { + renderFields: [ + 'topic', + 'hazard', + 'sector', + 'keyword', + 'framework', + 'instrument', + ], + validationFields: [ + 'topic', + 'hazard', + 'sector', + 'keyword', + 'framework', + 'instrument', + ], + }, + // Easy to extend for new corpus types + default: { + renderFields: [], + validationFields: [], + }, +} + +// Interface for rendering dynamic metadata fields +interface DynamicMetadataFieldProps { + fieldKey: string + taxonomyField: { + allowed_values?: string[] + allow_any?: boolean + allow_blanks?: boolean + } + control: Control + errors: FieldErrors + chakraStylesSelect?: any + corpusType?: string +} + +// Render a dynamic metadata field based on taxonomy configuration +export const renderDynamicMetadataField = ({ + fieldKey, + taxonomyField, + control, + errors, + chakraStylesSelect, + corpusType +}: DynamicMetadataFieldProps) => { + const allowedValues = taxonomyField.allowed_values || [] + const isAllowAny = taxonomyField.allow_any + const isAllowBlanks = taxonomyField.allow_blanks + + // Render free text input if allow_any is true + if (isAllowAny) { + return ( + + + {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} + + ( + + )} + /> + {errors[fieldKey] && ( + + {errors[fieldKey]?.message as string} + + )} + + ) + } + + // Special handling for author_type (radio group) + if (fieldKey === 'author_type') { + return ( + + Author Type + ( + + + {allowedValues.map((value) => ( + + {value} + + ))} + + + )} + /> + {errors[fieldKey] && ( + + Please select an author type + + )} + + ) + } + + // Render select box if allowed_values is not empty + if (allowedValues.length > 0) { + return ( + + + {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} + + ( + + )} + /> + {errors[fieldKey] && ( + + {errors[fieldKey]?.message as string} + + )} + {fieldKey !== 'author' && ( + + You can search and select multiple options + + )} + + ) + } + + // Fallback to default text input if no specific rendering rules apply + return ( + + + {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} + + ( + + )} + /> + {errors[fieldKey] && ( + + {errors[fieldKey]?.message as string} + + )} + + ) +} + +// Validation schema generation utility +export const generateDynamicValidationSchema = ( + taxonomy: any, + corpusInfo: any, + familySchema: any, +) => { + if (!taxonomy) return familySchema + + // Get validation fields based on corpus type, fallback to default + const { validationFields } = + CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] || + CORPUS_METADATA_CONFIG['default'] + + const metadataValidation = validationFields.reduce((acc, fieldKey) => { + const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] + + if (taxonomyField) { + // Get allowed values for the current field + const allowedValues = taxonomyField.allowed_values || [] + + // If allow_any is true, use a simple string validation + if (taxonomyField.allow_any) { + acc[fieldKey] = yup.string() + } + // For multi-select fields with allowed values + else if ( + allowedValues.length > 0 && + fieldKey !== 'author' && + fieldKey !== 'author_type' + ) { + acc[fieldKey] = yup.array().of(yup.string()) + } + // For single select or text fields + else { + // Use allow_blanks to determine if the field is required + acc[fieldKey] = taxonomyField.allow_blanks + ? yup.string() + : yup.string().required(`${fieldKey} is required`) + } + } + + return acc + }, {} as any) + + return familySchema.shape({ + ...metadataValidation, + }) +} diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index a8a0624..61d614f 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -69,12 +69,18 @@ import { EventForm } from './EventForm' import { canModify } from '@/utils/canModify' import { getCountries } from '@/utils/extractNestedGeographyData' import { decodeToken } from '@/utils/decodeToken' -import { generateOptions } from '@/utils/generateOptions' import { stripHtml } from '@/utils/stripHtml' import { familySchema } from '@/schemas/familySchema' import useCorpusFromConfig from '@/hooks/useCorpusFromConfig' +import { + renderDynamicMetadataField, + CORPUS_METADATA_CONFIG, + generateDynamicValidationSchema, + generateOptions, +} from './DynamicMetadataFields' + type TMultiSelect = { value: string label: string @@ -107,45 +113,6 @@ const getCollection = (collectionId: string, collections: ICollection[]) => { return collections.find((collection) => collection.import_id === collectionId) } -// Define a type for corpus metadata configuration -type CorpusMetadataConfig = { - [corpusType: string]: { - renderFields: string[] - validationFields: string[] - } -} - -// Centralized configuration for corpus metadata -const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { - 'Intl. agreements': { - renderFields: ['author', 'author_type'], - validationFields: ['author', 'author_type'], - }, - 'Laws and Policies': { - renderFields: [ - 'topic', - 'hazard', - 'sector', - 'keyword', - 'framework', - 'instrument', - ], - validationFields: [ - 'topic', - 'hazard', - 'sector', - 'keyword', - 'framework', - 'instrument', - ], - }, - // Easy to extend for new corpus types - default: { - renderFields: [], - validationFields: [], - }, -} - export const FamilyForm = ({ family: loadedFamily }: TProps) => { const [isLeavingModalOpen, setIsLeavingModalOpen] = useState(false) const [isFormSubmitting, setIsFormSubmitting] = useState(false) @@ -502,210 +469,22 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] if (!taxonomyField) return null - // Determine field rendering based on taxonomy configuration - const allowedValues = taxonomyField.allowed_values || [] - const isAllowAny = taxonomyField.allow_any - const isAllowBlanks = taxonomyField.allow_blanks - - // Render free text input if allow_any is true - if (isAllowAny) { - return ( - - - {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} - - ( - - )} - /> - {errors[fieldKey] && ( - - {errors[fieldKey]?.message as string} - - )} - - ) - } - - // Special handling for author_type (radio group) - if (fieldKey === 'author_type') { - return ( - - Author Type - ( - - - {allowedValues.map((value) => ( - - {value} - - ))} - - - )} - /> - {errors[fieldKey] && ( - - Please select an author type - - )} - - ) - } - - // Render select box if allowed_values is not empty - if (allowedValues.length > 0) { - return ( - - - {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} - - ( - - )} - /> - {errors[fieldKey] && ( - - {errors[fieldKey]?.message as string} - - )} - {fieldKey !== 'author' && ( - - You can search and select multiple options - - )} - - ) - } - - // Fallback to default text input if no specific rendering rules apply - return ( - - - {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} - - ( - - )} - /> - {errors[fieldKey] && ( - - {errors[fieldKey]?.message as string} - - )} - - ) + return renderDynamicMetadataField({ + fieldKey, + taxonomyField, + control, + errors, + chakraStylesSelect, + corpusType: corpusInfo.corpus_type, + }) }) .filter(Boolean) }, [corpusInfo, taxonomy, control, errors]) const dynamicValidationSchema = useMemo(() => { - if (!taxonomy) return familySchema - - // Get validation fields based on corpus type, fallback to default - const { validationFields } = - CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] || - CORPUS_METADATA_CONFIG['default'] - - const metadataValidation = validationFields.reduce((acc, fieldKey) => { - const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] - - if (taxonomyField) { - // Get allowed values for the current field - const allowedValues = taxonomyField.allowed_values || [] - - // If allow_any is true, use a simple string validation - if (taxonomyField.allow_any) { - acc[fieldKey] = yup.string() - } - // For multi-select fields with allowed values - else if ( - allowedValues.length > 0 && - fieldKey !== 'author' && - fieldKey !== 'author_type' - ) { - acc[fieldKey] = yup.array().of(yup.string()) - } - // For single select or text fields - else { - // Use allow_blanks to determine if the field is required - acc[fieldKey] = taxonomyField.allow_blanks - ? yup.string() - : yup.string().required(`${fieldKey} is required`) - } - } - - return acc - }, {} as any) - - return familySchema.shape({ - ...metadataValidation, - }) + return generateDynamicValidationSchema(taxonomy, corpusInfo, familySchema) }, [taxonomy, corpusInfo, familySchema]) - useEffect(() => { - if (taxonomy) { - // Get fields to reset based on corpus type - const { validationFields } = - CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] || - CORPUS_METADATA_CONFIG['default'] - - const currentValues = getValues() - const resetValues = validationFields.reduce((acc, field) => { - acc[field] = undefined - return acc - }, {} as any) - - reset( - { - ...currentValues, - ...resetValues, - }, - { - keepErrors: false, - keepDirty: true, - }, - ) - - // Update validation resolver - setValue('resolver', yupResolver(dynamicValidationSchema)) - } - }, [taxonomy, corpusInfo, dynamicValidationSchema]) - return ( <> {(configLoading || collectionsLoading) && ( @@ -912,6 +691,10 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { UNFCCC + + + MCF + From 0b717b9a057179b9aa62f467691a643c45ca6259 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:05:10 +0000 Subject: [PATCH 03/88] Rename from familySchema --- .../forms/DynamicMetadataFields.tsx | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index 2e4908d..1f0dc8f 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -10,15 +10,15 @@ import { HStack, Box, AbsoluteCenter, - Divider + Divider, } from '@chakra-ui/react' import { Controller, Control, FieldErrors } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' import * as yup from 'yup' // Utility function to generate select options -export const generateOptions = (values: string[]) => - values.map(value => ({ value, label: value })) +export const generateOptions = (values: string[]) => + values.map((value) => ({ value, label: value })) // Configuration type for corpus metadata export type CorpusMetadataConfig = { @@ -80,7 +80,7 @@ export const renderDynamicMetadataField = ({ control, errors, chakraStylesSelect, - corpusType + corpusType, }: DynamicMetadataFieldProps) => { const allowedValues = taxonomyField.allowed_values || [] const isAllowAny = taxonomyField.allow_any @@ -97,11 +97,7 @@ export const renderDynamicMetadataField = ({ control={control} name={fieldKey} render={({ field }) => ( - + )} /> {errors[fieldKey] && ( @@ -139,9 +135,7 @@ export const renderDynamicMetadataField = ({ )} /> {errors[fieldKey] && ( - - Please select an author type - + Please select an author type )} ) @@ -161,9 +155,7 @@ export const renderDynamicMetadataField = ({ ( - + )} /> {errors[fieldKey] && ( @@ -214,9 +202,9 @@ export const renderDynamicMetadataField = ({ export const generateDynamicValidationSchema = ( taxonomy: any, corpusInfo: any, - familySchema: any, + schema: any, ) => { - if (!taxonomy) return familySchema + if (!taxonomy) return schema // Get validation fields based on corpus type, fallback to default const { validationFields } = @@ -254,7 +242,7 @@ export const generateDynamicValidationSchema = ( return acc }, {} as any) - return familySchema.shape({ + return schema.shape({ ...metadataValidation, }) } From 911450baeee563ba0b4462ad170418a9243e6f4b Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:12:38 +0000 Subject: [PATCH 04/88] Fix style --- src/components/forms/DynamicMetadataFields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index 1f0dc8f..113f06e 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -97,7 +97,7 @@ export const renderDynamicMetadataField = ({ control={control} name={fieldKey} render={({ field }) => ( - + )} /> {errors[fieldKey] && ( From 27156fb8b09356377ce82cf33c178a2a08e8be15 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:21:39 +0000 Subject: [PATCH 05/88] FIx formatting --- src/components/forms/DynamicMetadataFields.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index 113f06e..42b4ef3 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { FormControl, FormLabel, @@ -8,9 +7,6 @@ import { RadioGroup, Radio, HStack, - Box, - AbsoluteCenter, - Divider, } from '@chakra-ui/react' import { Controller, Control, FieldErrors } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' @@ -28,7 +24,7 @@ export type CorpusMetadataConfig = { } } -// Centralized configuration for corpus metadata +// Centralised configuration for corpus metadata export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { 'Intl. agreements': { renderFields: ['author', 'author_type'], From 987debe1577379f32923cf4220af8aac152f14ad Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:33:46 +0000 Subject: [PATCH 06/88] Split schema generation to new file --- .../forms/DynamicMetadataFields.tsx | 50 ----------- src/components/forms/FamilyForm.tsx | 2 +- src/schemas/dynamicValidationSchema.ts | 89 +++++++++++++++++++ 3 files changed, 90 insertions(+), 51 deletions(-) create mode 100644 src/schemas/dynamicValidationSchema.ts diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index 42b4ef3..f4198d4 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -10,7 +10,6 @@ import { } from '@chakra-ui/react' import { Controller, Control, FieldErrors } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' -import * as yup from 'yup' // Utility function to generate select options export const generateOptions = (values: string[]) => @@ -193,52 +192,3 @@ export const renderDynamicMetadataField = ({ ) } - -// Validation schema generation utility -export const generateDynamicValidationSchema = ( - taxonomy: any, - corpusInfo: any, - schema: any, -) => { - if (!taxonomy) return schema - - // Get validation fields based on corpus type, fallback to default - const { validationFields } = - CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] || - CORPUS_METADATA_CONFIG['default'] - - const metadataValidation = validationFields.reduce((acc, fieldKey) => { - const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] - - if (taxonomyField) { - // Get allowed values for the current field - const allowedValues = taxonomyField.allowed_values || [] - - // If allow_any is true, use a simple string validation - if (taxonomyField.allow_any) { - acc[fieldKey] = yup.string() - } - // For multi-select fields with allowed values - else if ( - allowedValues.length > 0 && - fieldKey !== 'author' && - fieldKey !== 'author_type' - ) { - acc[fieldKey] = yup.array().of(yup.string()) - } - // For single select or text fields - else { - // Use allow_blanks to determine if the field is required - acc[fieldKey] = taxonomyField.allow_blanks - ? yup.string() - : yup.string().required(`${fieldKey} is required`) - } - } - - return acc - }, {} as any) - - return schema.shape({ - ...metadataValidation, - }) -} diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 61d614f..5e8cc0b 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -77,9 +77,9 @@ import useCorpusFromConfig from '@/hooks/useCorpusFromConfig' import { renderDynamicMetadataField, CORPUS_METADATA_CONFIG, - generateDynamicValidationSchema, generateOptions, } from './DynamicMetadataFields' +import { generateDynamicValidationSchema } from '@/schemas/dynamicValidationSchema' type TMultiSelect = { value: string diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts new file mode 100644 index 0000000..ec130a8 --- /dev/null +++ b/src/schemas/dynamicValidationSchema.ts @@ -0,0 +1,89 @@ +import * as yup from 'yup' + +// Configuration type for corpus metadata +export type CorpusMetadataConfig = { + [corpusType: string]: { + renderFields: string[] + validationFields: string[] + } +} + +// Centralized configuration for corpus metadata +export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { + 'Intl. agreements': { + renderFields: ['author', 'author_type'], + validationFields: ['author', 'author_type'], + }, + 'Laws and Policies': { + renderFields: [ + 'topic', + 'hazard', + 'sector', + 'keyword', + 'framework', + 'instrument', + ], + validationFields: [ + 'topic', + 'hazard', + 'sector', + 'keyword', + 'framework', + 'instrument', + ], + }, + // Easy to extend for new corpus types + default: { + renderFields: [], + validationFields: [], + }, +} + +// Validation schema generation utility +export const generateDynamicValidationSchema = ( + taxonomy: any, + corpusInfo: any, + schema: any, +) => { + if (!taxonomy) return schema + + // Get validation fields based on corpus type, fallback to default + const { validationFields } = + CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] || + CORPUS_METADATA_CONFIG['default'] + + const metadataValidation = validationFields.reduce((acc, fieldKey) => { + const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] + + if (taxonomyField) { + // Get allowed values for the current field + const allowedValues = taxonomyField.allowed_values || [] + + // If allow_any is true, use a simple string validation + if (taxonomyField.allow_any) { + acc[fieldKey] = yup.string() + } + // For multi-select fields with allowed values + else if ( + allowedValues.length > 0 && + fieldKey !== 'author' && + fieldKey !== 'author_type' + ) { + acc[fieldKey] = yup.array().of(yup.string()) + } + // For single select or text fields + else { + // Use allow_blanks to determine if the field is required + acc[fieldKey] = taxonomyField.allow_blanks + ? yup.string() + : yup.string().required(`${fieldKey} is required`) + } + } + + return acc + }, {} as any) + + return schema.shape({ + ...metadataValidation, + }) +} \ No newline at end of file From 98088ca1222dcc4bc0d19b94e3519f678e06ed0d Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:35:27 +0000 Subject: [PATCH 07/88] Optimise image --- public/favicon.png | Bin 39322 -> 16296 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/favicon.png b/public/favicon.png index b831086e7016fba932e65584e11a186998c22970..e9fadb4a6554769f7be0f798ea9ab806dae954dc 100644 GIT binary patch literal 16296 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 DA;3t% literal 39322 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 Date: Tue, 3 Dec 2024 18:35:46 +0000 Subject: [PATCH 08/88] Auto fix --- .../forms/DynamicMetadataFields.tsx | 39 ------------------- src/components/forms/FamilyForm.tsx | 6 ++- src/schemas/dynamicValidationSchema.ts | 4 +- 3 files changed, 6 insertions(+), 43 deletions(-) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index f4198d4..a6f76e5 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -15,45 +15,6 @@ import { Select as CRSelect } from 'chakra-react-select' export const generateOptions = (values: string[]) => values.map((value) => ({ value, label: value })) -// Configuration type for corpus metadata -export type CorpusMetadataConfig = { - [corpusType: string]: { - renderFields: string[] - validationFields: string[] - } -} - -// Centralised configuration for corpus metadata -export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { - 'Intl. agreements': { - renderFields: ['author', 'author_type'], - validationFields: ['author', 'author_type'], - }, - 'Laws and Policies': { - renderFields: [ - 'topic', - 'hazard', - 'sector', - 'keyword', - 'framework', - 'instrument', - ], - validationFields: [ - 'topic', - 'hazard', - 'sector', - 'keyword', - 'framework', - 'instrument', - ], - }, - // Easy to extend for new corpus types - default: { - renderFields: [], - validationFields: [], - }, -} - // Interface for rendering dynamic metadata fields interface DynamicMetadataFieldProps { fieldKey: string diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 5e8cc0b..1b909cb 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -76,10 +76,12 @@ import useCorpusFromConfig from '@/hooks/useCorpusFromConfig' import { renderDynamicMetadataField, - CORPUS_METADATA_CONFIG, generateOptions, } from './DynamicMetadataFields' -import { generateDynamicValidationSchema } from '@/schemas/dynamicValidationSchema' +import { + CORPUS_METADATA_CONFIG, + generateDynamicValidationSchema, +} from '@/schemas/dynamicValidationSchema' type TMultiSelect = { value: string diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index ec130a8..c7300ad 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -8,7 +8,7 @@ export type CorpusMetadataConfig = { } } -// Centralized configuration for corpus metadata +// Centralised configuration for corpus metadata export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { 'Intl. agreements': { renderFields: ['author', 'author_type'], @@ -86,4 +86,4 @@ export const generateDynamicValidationSchema = ( return schema.shape({ ...metadataValidation, }) -} \ No newline at end of file +} From f59001c5058fd23ffffe869d0ed1b0234c85449f Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:46:05 +0000 Subject: [PATCH 09/88] Add AF schema --- src/components/forms/FamilyForm.tsx | 2 ++ src/schemas/dynamicValidationSchema.ts | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 1b909cb..c2852fb 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -187,6 +187,8 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { [orgName, isSuperUser, userAccess], ) + console.log(loadedFamily) + // Family handlers const handleFormSubmission = async (family: IFamilyForm) => { setIsFormSubmitting(true) diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index c7300ad..49f73d2 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -14,6 +14,28 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { renderFields: ['author', 'author_type'], validationFields: ['author', 'author_type'], }, + 'AF': { + renderFields: [ + 'region', + 'sector', + 'status', + 'implementing_agency', + 'project_id', + 'project_url', + 'project_value_co_financing', + 'project_value_fund_spend', + ], + validationFields: [ + 'region', + 'sector', + 'status', + 'implementing_agency', + 'project_id', + 'project_url', + 'project_value_co_financing', + 'project_value_fund_spend', + ], + }, 'Laws and Policies': { renderFields: [ 'topic', From b5ea11604fe925650ee1909244a5640c3e592b5b Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:28:01 +0000 Subject: [PATCH 10/88] Use taxonomy to determine required and free text fields --- .../forms/DynamicMetadataFields.tsx | 197 ++++++++---------- src/components/forms/FamilyForm.tsx | 94 ++++++--- src/schemas/dynamicValidationSchema.ts | 145 +++++++------ 3 files changed, 234 insertions(+), 202 deletions(-) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index a6f76e5..ed36cd8 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -10,11 +10,28 @@ import { } from '@chakra-ui/react' import { Controller, Control, FieldErrors } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' +import * as yup from 'yup' +import { + generateDynamicValidationSchema, + CORPUS_METADATA_CONFIG, + CorpusMetadataConfig, + FieldType, +} from '@/schemas/dynamicValidationSchema' +import React from 'react' +import { chakraStylesSelect } from '@/styles/chakra' // Utility function to generate select options export const generateOptions = (values: string[]) => values.map((value) => ({ value, label: value })) +// Utility function to format field labels +const formatFieldLabel = (key: string): string => { + return key + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' ') +} + // Interface for rendering dynamic metadata fields interface DynamicMetadataFieldProps { fieldKey: string @@ -24,132 +41,88 @@ interface DynamicMetadataFieldProps { allow_blanks?: boolean } control: Control - errors: FieldErrors - chakraStylesSelect?: any - corpusType?: string + errors: FieldErrors + fieldType: FieldType } -// Render a dynamic metadata field based on taxonomy configuration -export const renderDynamicMetadataField = ({ - fieldKey, - taxonomyField, - control, - errors, - chakraStylesSelect, - corpusType, -}: DynamicMetadataFieldProps) => { - const allowedValues = taxonomyField.allowed_values || [] - const isAllowAny = taxonomyField.allow_any - const isAllowBlanks = taxonomyField.allow_blanks +export const DynamicMetadataField: React.FC = + React.memo(({ fieldKey, taxonomyField, control, errors, fieldType }) => { + const allowedValues = taxonomyField.allowed_values || [] + const isAllowAny = taxonomyField.allow_any + const isAllowBlanks = taxonomyField.allow_blanks - // Render free text input if allow_any is true - if (isAllowAny) { - return ( - - - {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} - - ( - - )} - /> - {errors[fieldKey] && ( - - {errors[fieldKey]?.message as string} - - )} - - ) - } + const renderFieldByType = () => { + if (isAllowAny) { + return renderTextField() + } - // Special handling for author_type (radio group) - if (fieldKey === 'author_type') { - return ( - - Author Type - ( - - - {allowedValues.map((value) => ( - - {value} - - ))} - - - )} - /> - {errors[fieldKey] && ( - Please select an author type + switch (fieldType) { + case FieldType.MULTI_SELECT: + case FieldType.SINGLE_SELECT: + return renderSelectField() + case FieldType.TEXT: + return renderTextField() + case FieldType.NUMBER: + return renderNumberField() + default: + return renderTextField() + } + } + + const renderSelectField = () => ( + ( + )} - + /> ) - } - // Render select box if allowed_values is not empty - if (allowedValues.length > 0) { - return ( - - - {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} - - ( - - )} - /> - {errors[fieldKey] && ( - - {errors[fieldKey]?.message as string} - - )} - {fieldKey !== 'author' && ( - - You can search and select multiple options - + const renderTextField = () => ( + ( + )} - + /> ) - } - // Fallback to default text input if no specific rendering rules apply - return ( - - - {fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1)} - + const renderNumberField = () => ( ( - + )} /> - {errors[fieldKey] && ( + ) + + return ( + + {formatFieldLabel(fieldKey)} + {fieldType === FieldType.MULTI_SELECT && ( + + You are able to search and can select multiple options + + )} + {renderFieldByType()} - {errors[fieldKey]?.message as string} + {errors[fieldKey] && `${fieldKey} is required`} - )} - - ) -} + + ) + }) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index c2852fb..4c13eba 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -74,13 +74,11 @@ import { stripHtml } from '@/utils/stripHtml' import { familySchema } from '@/schemas/familySchema' import useCorpusFromConfig from '@/hooks/useCorpusFromConfig' -import { - renderDynamicMetadataField, - generateOptions, -} from './DynamicMetadataFields' +import { DynamicMetadataField, generateOptions } from './DynamicMetadataFields' import { CORPUS_METADATA_CONFIG, generateDynamicValidationSchema, + FieldType, } from '@/schemas/dynamicValidationSchema' type TMultiSelect = { @@ -159,14 +157,45 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { watchCorpus?.value, ) - console.log('config?.corpora', config?.corpora) + console.log('loadedFamily', loadedFamily) + console.log('corpusInfo', corpusInfo) + console.log('CORPUS_METADATA_CONFIG', CORPUS_METADATA_CONFIG) + + useEffect(() => { + if (loadedFamily && corpusInfo) { + console.log('Loaded Family Metadata:', loadedFamily.metadata) + console.log('Corpus Type:', corpusInfo.corpus_type) + console.log('Render Fields:', + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields + ) + } + }, [loadedFamily, corpusInfo]) + + useEffect(() => { + if (loadedFamily) { + console.log('Full Loaded Family Metadata:', loadedFamily.metadata) + console.log('Metadata Keys:', Object.keys(loadedFamily.metadata)) + + // Detailed logging for each metadata field + const metadataFields = [ + 'topic', 'hazard', 'sector', 'keyword', + 'framework', 'instrument', 'author', 'author_type' + ] + + metadataFields.forEach(field => { + console.log(`${field} exists:`, field in loadedFamily.metadata) + if (field in loadedFamily.metadata) { + console.log(`${field} value:`, loadedFamily.metadata[field]) + } + }) + } + }, [loadedFamily]) const corpusTitle = loadedFamily ? loadedFamily?.corpus_title : corpusInfo?.title const taxonomy = useTaxonomy(corpusInfo?.corpus_type, corpusInfo?.taxonomy) - console.log('corpusInfo', corpusInfo) console.log('taxonomy', taxonomy) const userToken = useMemo(() => { @@ -187,8 +216,6 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { [orgName, isSuperUser, userAccess], ) - console.log(loadedFamily) - // Family handlers const handleFormSubmission = async (family: IFamilyForm) => { setIsFormSubmitting(true) @@ -462,27 +489,21 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { const renderDynamicMetadataFields = useCallback(() => { if (!corpusInfo || !taxonomy) return null - // Get render fields based on corpus type, fallback to default - const { renderFields } = - CORPUS_METADATA_CONFIG[corpusInfo.corpus_type] || - CORPUS_METADATA_CONFIG['default'] - - return renderFields - .map((fieldKey) => { - // Check if the field exists in the taxonomy - const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] - if (!taxonomyField) return null - - return renderDynamicMetadataField({ - fieldKey, - taxonomyField, - control, - errors, - chakraStylesSelect, - corpusType: corpusInfo.corpus_type, - }) - }) - .filter(Boolean) + return ( + taxonomy && + Object.entries( + CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type]?.renderFields || {}, + ).map(([fieldKey, fieldConfig]) => ( + + )) + ) }, [corpusInfo, taxonomy, control, errors]) const dynamicValidationSchema = useMemo(() => { @@ -716,7 +737,20 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { )} - {renderDynamicMetadataFields()} + {taxonomy && + Object.entries( + CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] + ?.renderFields || {}, + ).map(([fieldKey, fieldConfig]) => ( + + ))} diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 49f73d2..f18d78c 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -1,9 +1,24 @@ import * as yup from 'yup' -// Configuration type for corpus metadata +// Enum for field types to ensure type safety and scalability +export enum FieldType { + TEXT = 'text', + MULTI_SELECT = 'multi_select', + SINGLE_SELECT = 'single_select', + NUMBER = 'number', + DATE = 'date', +} + +// Enhanced configuration type for corpus metadata export type CorpusMetadataConfig = { [corpusType: string]: { - renderFields: string[] + renderFields: { + [fieldKey: string]: { + type: FieldType + label?: string + allowedValues?: string[] + } + } validationFields: string[] } } @@ -11,33 +26,22 @@ export type CorpusMetadataConfig = { // Centralised configuration for corpus metadata export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { 'Intl. agreements': { - renderFields: ['author', 'author_type'], + renderFields: { + author: { type: FieldType.TEXT }, + author_type: { type: FieldType.SINGLE_SELECT }, + }, validationFields: ['author', 'author_type'], }, - 'AF': { - renderFields: [ - 'region', - 'sector', - 'status', - 'implementing_agency', - 'project_id', - 'project_url', - 'project_value_co_financing', - 'project_value_fund_spend', - ], - validationFields: [ - 'region', - 'sector', - 'status', - 'implementing_agency', - 'project_id', - 'project_url', - 'project_value_co_financing', - 'project_value_fund_spend', - ], - }, 'Laws and Policies': { - renderFields: [ + renderFields: { + topic: { type: FieldType.MULTI_SELECT }, + hazard: { type: FieldType.MULTI_SELECT }, + sector: { type: FieldType.MULTI_SELECT }, + keyword: { type: FieldType.MULTI_SELECT }, + framework: { type: FieldType.MULTI_SELECT }, + instrument: { type: FieldType.MULTI_SELECT }, + }, + validationFields: [ 'topic', 'hazard', 'sector', @@ -45,18 +49,31 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { 'framework', 'instrument', ], + }, + AF: { + renderFields: { + region: { type: FieldType.MULTI_SELECT }, + sector: { type: FieldType.MULTI_SELECT }, + implementing_agency: { type: FieldType.MULTI_SELECT }, + status: { type: FieldType.SINGLE_SELECT }, + project_id: { type: FieldType.TEXT }, + project_url: { type: FieldType.TEXT }, + project_value_co_financing: { type: FieldType.NUMBER }, + project_value_fund_spend: { type: FieldType.NUMBER }, + }, validationFields: [ - 'topic', - 'hazard', + 'project_id', + 'project_url', + 'region', 'sector', - 'keyword', - 'framework', - 'instrument', + 'status', + 'implementing_agency', + 'project_value_co_financing', + 'project_value_fund_spend', ], }, - // Easy to extend for new corpus types default: { - renderFields: [], + renderFields: {}, validationFields: [], }, } @@ -69,36 +86,44 @@ export const generateDynamicValidationSchema = ( ) => { if (!taxonomy) return schema - // Get validation fields based on corpus type, fallback to default - const { validationFields } = - CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] || - CORPUS_METADATA_CONFIG['default'] - - const metadataValidation = validationFields.reduce((acc, fieldKey) => { + const metadataValidation = CORPUS_METADATA_CONFIG[ + corpusInfo?.corpus_type + ]?.validationFields.reduce((acc, fieldKey) => { const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] + const renderField = + CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type]?.renderFields[fieldKey] if (taxonomyField) { - // Get allowed values for the current field - const allowedValues = taxonomyField.allowed_values || [] - - // If allow_any is true, use a simple string validation - if (taxonomyField.allow_any) { - acc[fieldKey] = yup.string() - } - // For multi-select fields with allowed values - else if ( - allowedValues.length > 0 && - fieldKey !== 'author' && - fieldKey !== 'author_type' - ) { - acc[fieldKey] = yup.array().of(yup.string()) - } - // For single select or text fields - else { - // Use allow_blanks to determine if the field is required - acc[fieldKey] = taxonomyField.allow_blanks - ? yup.string() - : yup.string().required(`${fieldKey} is required`) + // Determine validation based on field type and allow_blanks + switch (renderField?.type) { + case FieldType.TEXT: + acc[fieldKey] = taxonomyField.allow_blanks + ? yup.string() + : yup.string().required(`${fieldKey} is required`) + break + case FieldType.MULTI_SELECT: + acc[fieldKey] = taxonomyField.allow_blanks + ? yup.array().of(yup.string()) + : yup.array().of(yup.string()).min(1, `${fieldKey} is required`) + break + case FieldType.SINGLE_SELECT: + acc[fieldKey] = taxonomyField.allow_blanks + ? yup.string() + : yup.string().required(`${fieldKey} is required`) + break + case FieldType.NUMBER: + acc[fieldKey] = taxonomyField.allow_blanks + ? yup.number() + : yup.number().required(`${fieldKey} is required`) + break + case FieldType.DATE: + acc[fieldKey] = taxonomyField.allow_blanks + ? yup.date() + : yup.date().required(`${fieldKey} is required`) + break + default: + // Fallback for unspecified types + acc[fieldKey] = yup.string() } } From 92e9e0a4756fd04378a07054284107202bd3bace Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:30:53 +0000 Subject: [PATCH 11/88] Fix formatting --- .../forms/DynamicMetadataFields.tsx | 43 +++++++++---------- src/components/forms/FamilyForm.tsx | 24 ++++++----- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index ed36cd8..bc6f027 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -4,34 +4,13 @@ import { FormErrorMessage, FormHelperText, Input, - RadioGroup, - Radio, - HStack, } from '@chakra-ui/react' import { Controller, Control, FieldErrors } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' -import * as yup from 'yup' -import { - generateDynamicValidationSchema, - CORPUS_METADATA_CONFIG, - CorpusMetadataConfig, - FieldType, -} from '@/schemas/dynamicValidationSchema' +import { FieldType } from '@/schemas/dynamicValidationSchema' import React from 'react' import { chakraStylesSelect } from '@/styles/chakra' -// Utility function to generate select options -export const generateOptions = (values: string[]) => - values.map((value) => ({ value, label: value })) - -// Utility function to format field labels -const formatFieldLabel = (key: string): string => { - return key - .split('_') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(' ') -} - // Interface for rendering dynamic metadata fields interface DynamicMetadataFieldProps { fieldKey: string @@ -69,6 +48,10 @@ export const DynamicMetadataField: React.FC = } } + // Utility function to generate select options + const generateOptions = (values: string[]) => + values.map((value) => ({ value, label: value })) + const renderSelectField = () => ( = /> ) + // Utility function to format field labels + const formatFieldLabel = (key: string): string => { + return key + .split('_') + .map( + (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ) + .join(' ') + } + return ( - + {formatFieldLabel(fieldKey)} {fieldType === FieldType.MULTI_SELECT && ( diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 4c13eba..86f0b0b 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -2,7 +2,6 @@ import { useEffect, useState, useMemo, useCallback } from 'react' import { useForm, SubmitHandler, Controller } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { useBlocker, useNavigate } from 'react-router-dom' -import * as yup from 'yup' import { IError, TFamilyFormPost, @@ -27,7 +26,6 @@ import useCollections from '@/hooks/useCollections' import { Box, FormControl, - FormHelperText, FormLabel, HStack, Input, @@ -78,7 +76,6 @@ import { DynamicMetadataField, generateOptions } from './DynamicMetadataFields' import { CORPUS_METADATA_CONFIG, generateDynamicValidationSchema, - FieldType, } from '@/schemas/dynamicValidationSchema' type TMultiSelect = { @@ -165,8 +162,9 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { if (loadedFamily && corpusInfo) { console.log('Loaded Family Metadata:', loadedFamily.metadata) console.log('Corpus Type:', corpusInfo.corpus_type) - console.log('Render Fields:', - CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields + console.log( + 'Render Fields:', + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields, ) } }, [loadedFamily, corpusInfo]) @@ -175,14 +173,20 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { if (loadedFamily) { console.log('Full Loaded Family Metadata:', loadedFamily.metadata) console.log('Metadata Keys:', Object.keys(loadedFamily.metadata)) - + // Detailed logging for each metadata field const metadataFields = [ - 'topic', 'hazard', 'sector', 'keyword', - 'framework', 'instrument', 'author', 'author_type' + 'topic', + 'hazard', + 'sector', + 'keyword', + 'framework', + 'instrument', + 'author', + 'author_type', ] - - metadataFields.forEach(field => { + + metadataFields.forEach((field) => { console.log(`${field} exists:`, field in loadedFamily.metadata) if (field in loadedFamily.metadata) { console.log(`${field} value:`, loadedFamily.metadata[field]) From 582243655d960302554298c1b703ce1b0131dce2 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:41:42 +0000 Subject: [PATCH 12/88] Fix eslint errors --- src/components/forms/FamilyForm.tsx | 37 ++++++++++++++++---------- src/schemas/dynamicValidationSchema.ts | 35 +++++++++++++++++------- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 86f0b0b..2d092b7 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -131,10 +131,9 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { reset, setError, setValue, - getValues, formState: { errors, isSubmitting }, formState: { dirtyFields }, - } = useForm({ + } = useForm({ resolver: yupResolver(familySchema), }) const [editingEntity, setEditingEntity] = useState() @@ -423,27 +422,33 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { } : undefined, topic: - 'topic' in loadedFamily.metadata + 'topic' in loadedFamily.metadata && + Array.isArray(loadedFamily.metadata.topic) ? generateOptions(loadedFamily.metadata.topic) : [], hazard: - 'hazard' in loadedFamily.metadata + 'hazard' in loadedFamily.metadata && + Array.isArray(loadedFamily.metadata.hazard) ? generateOptions(loadedFamily.metadata.hazard) : [], sector: - 'sector' in loadedFamily.metadata + 'sector' in loadedFamily.metadata && + Array.isArray(loadedFamily.metadata.sector) ? generateOptions(loadedFamily.metadata.sector) : [], keyword: - 'keyword' in loadedFamily.metadata + 'keyword' in loadedFamily.metadata && + Array.isArray(loadedFamily.metadata.keyword) ? generateOptions(loadedFamily.metadata.keyword) : [], framework: - 'framework' in loadedFamily.metadata + 'framework' in loadedFamily.metadata && + Array.isArray(loadedFamily.metadata.framework) ? generateOptions(loadedFamily.metadata.framework) : [], instrument: - 'instrument' in loadedFamily.metadata + 'instrument' in loadedFamily.metadata && + Array.isArray(loadedFamily.metadata.instrument) ? generateOptions(loadedFamily.metadata.instrument) : [], author: @@ -496,7 +501,8 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { return ( taxonomy && Object.entries( - CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type]?.renderFields || {}, + (CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type]?.renderFields || + {}) as Record, ).map(([fieldKey, fieldConfig]) => ( { ) }, [corpusInfo, taxonomy, control, errors]) - const dynamicValidationSchema = useMemo(() => { - return generateDynamicValidationSchema(taxonomy, corpusInfo, familySchema) - }, [taxonomy, corpusInfo, familySchema]) + const validationSchema = useMemo(() => { + return generateDynamicValidationSchema(taxonomy, corpusInfo) + }, [taxonomy, corpusInfo]) return ( <> @@ -743,8 +749,11 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { )} {taxonomy && Object.entries( - CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] - ?.renderFields || {}, + (CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] + ?.renderFields || {}) as Record< + string, + { type: FieldType } + >, ).map(([fieldKey, fieldConfig]) => ( + // Validation schema generation utility export const generateDynamicValidationSchema = ( - taxonomy: any, - corpusInfo: any, - schema: any, -) => { + taxonomy: Taxonomy, + corpusInfo: CorpusInfo, + schema: ValidationSchema, +): ValidationSchema => { if (!taxonomy) return schema const metadataValidation = CORPUS_METADATA_CONFIG[ corpusInfo?.corpus_type - ]?.validationFields.reduce((acc, fieldKey) => { - const taxonomyField = taxonomy[fieldKey as keyof typeof taxonomy] + ]?.validationFields.reduce>((acc, fieldKey) => { + const taxonomyField = taxonomy[fieldKey] const renderField = CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type]?.renderFields[fieldKey] @@ -128,9 +145,7 @@ export const generateDynamicValidationSchema = ( } return acc - }, {} as any) + }, {}) - return schema.shape({ - ...metadataValidation, - }) + return schema.shape(metadataValidation) } From a78df90ff5f27d6955b7211e063d6e9d2d6a7771 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:26:54 +0000 Subject: [PATCH 13/88] Dynamically initialise AF metadata --- src/components/forms/DynamicMetadataField.tsx | 64 ++ src/components/forms/FamilyForm.tsx | 911 ++++++------------ src/components/forms/ReadOnlyFields.tsx | 47 + .../forms/fields/RadioGroupField.tsx | 54 ++ src/components/forms/fields/SelectField.tsx | 69 ++ src/components/forms/fields/TextField.tsx | 40 + src/components/forms/fields/WYSIWYGField.tsx | 49 + .../forms/modals/UnsavedChangesModal.tsx | 42 + .../forms/sections/DocumentSection.tsx | 74 ++ .../forms/sections/EventSection.tsx | 38 + .../forms/sections/MetadataSection.tsx | 81 ++ src/schemas/dynamicValidationSchema.ts | 153 ++- src/schemas/familySchema.ts | 34 +- src/types/metadata.ts | 53 + src/utils/metadataUtils.ts | 17 + src/views/family/Family.tsx | 3 - 16 files changed, 1038 insertions(+), 691 deletions(-) create mode 100644 src/components/forms/DynamicMetadataField.tsx create mode 100644 src/components/forms/ReadOnlyFields.tsx create mode 100644 src/components/forms/fields/RadioGroupField.tsx create mode 100644 src/components/forms/fields/SelectField.tsx create mode 100644 src/components/forms/fields/TextField.tsx create mode 100644 src/components/forms/fields/WYSIWYGField.tsx create mode 100644 src/components/forms/modals/UnsavedChangesModal.tsx create mode 100644 src/components/forms/sections/DocumentSection.tsx create mode 100644 src/components/forms/sections/EventSection.tsx create mode 100644 src/components/forms/sections/MetadataSection.tsx create mode 100644 src/types/metadata.ts create mode 100644 src/utils/metadataUtils.ts diff --git a/src/components/forms/DynamicMetadataField.tsx b/src/components/forms/DynamicMetadataField.tsx new file mode 100644 index 0000000..0ddcc1d --- /dev/null +++ b/src/components/forms/DynamicMetadataField.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { + FormControl, + FormLabel, + FormErrorMessage, + FormHelperText, +} from '@chakra-ui/react' +import { DynamicMetadataFieldProps, FieldType } from '@/types/metadata' +import { formatFieldLabel } from '@/utils/metadataUtils' +import { SelectField } from './fields/SelectField' +import { TextField } from './fields/TextField' + +export const DynamicMetadataField = >({ + fieldKey, + taxonomyField, + control, + errors, + fieldType, +}: DynamicMetadataFieldProps): React.ReactElement => { + const { allowed_values = [], allow_any = false, allow_blanks = true } = taxonomyField + + const renderField = () => { + if (allow_any) { + return name={fieldKey} control={control} /> + } + + switch (fieldType) { + case FieldType.MULTI_SELECT: + case FieldType.SINGLE_SELECT: + return ( + + name={fieldKey} + control={control} + options={allowed_values} + isMulti={fieldType === FieldType.MULTI_SELECT} + /> + ) + case FieldType.NUMBER: + return name={fieldKey} control={control} type="number" /> + case FieldType.TEXT: + default: + return name={fieldKey} control={control} /> + } + } + + return ( + + {formatFieldLabel(fieldKey)} + {fieldType === FieldType.MULTI_SELECT && ( + + You are able to search and can select multiple options + + )} + {renderField()} + + {errors[fieldKey] && `${fieldKey} is required`} + + + ) +} diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 2d092b7..a39102e 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -2,36 +2,12 @@ import { useEffect, useState, useMemo, useCallback } from 'react' import { useForm, SubmitHandler, Controller } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { useBlocker, useNavigate } from 'react-router-dom' -import { - IError, - TFamilyFormPost, - TFamilyFormPostMetadata, - IUNFCCCMetadata, - ICCLWMetadata, - TFamily, - IDocument, - IEvent, - ICollection, - IConfigCorpus, - IDecodedToken, -} from '@/interfaces' - -import { createFamily, updateFamily } from '@/api/Families' -import { deleteDocument } from '@/api/Documents' - -import useConfig from '@/hooks/useConfig' -import useTaxonomy from '@/hooks/useTaxonomy' -import useCollections from '@/hooks/useCollections' - import { Box, FormControl, FormLabel, HStack, Input, - Radio, - RadioGroup, - Select, VStack, Text, Button, @@ -52,65 +28,52 @@ import { ModalCloseButton, } from '@chakra-ui/react' import { WarningIcon } from '@chakra-ui/icons' -import { Select as CRSelect } from 'chakra-react-select' -import { chakraStylesSelect } from '@/styles/chakra' -import { Loader } from '../Loader' -import { FamilyDocument } from '../family/FamilyDocument' -import { ApiError } from '../feedback/ApiError' -import { WYSIWYG } from '../form-components/WYSIWYG' -import { FamilyEventList } from '../lists/FamilyEventList' -import { EventEditDrawer } from '../drawers/EventEditDrawer' -import { DocumentEditDrawer } from '../drawers/DocumentEditDrawer' -import { DocumentForm } from './DocumentForm' -import { EventForm } from './EventForm' +import { familySchema } from '@/schemas/familySchema' +import useCorpusFromConfig from '@/hooks/useCorpusFromConfig' +import useConfig from '@/hooks/useConfig' +import useTaxonomy from '@/hooks/useTaxonomy' +import useCollections from '@/hooks/useCollections' + +import { DynamicMetadataField } from './DynamicMetadataFields' +import { generateOptions } from '@/utils/generateOptions' +import { SelectField } from './fields/SelectField' +import { TextField } from './fields/TextField' +import { RadioGroupField } from './fields/RadioGroupField' +import { WYSIWYGField } from './fields/WYSIWYGField' +import { MetadataSection } from './sections/MetadataSection' +import { DocumentSection } from './sections/DocumentSection' +import { EventSection } from './sections/EventSection' +import { UnsavedChangesModal } from './modals/UnsavedChangesModal' +import { ReadOnlyFields } from './ReadOnlyFields' + +import { + IFamilyForm, + TFamilyFormPost, + TFamilyFormPostMetadata, + IUNFCCCMetadata, + ICCLWMetadata, +} from '@/types/metadata' +import { TChildEntity } from '@/types/entities' import { canModify } from '@/utils/canModify' import { getCountries } from '@/utils/extractNestedGeographyData' import { decodeToken } from '@/utils/decodeToken' import { stripHtml } from '@/utils/stripHtml' - -import { familySchema } from '@/schemas/familySchema' -import useCorpusFromConfig from '@/hooks/useCorpusFromConfig' - -import { DynamicMetadataField, generateOptions } from './DynamicMetadataFields' import { CORPUS_METADATA_CONFIG, generateDynamicValidationSchema, } from '@/schemas/dynamicValidationSchema' +import { createFamily, updateFamily } from '@/api/Families' +import { deleteDocument } from '@/api/Documents' +import { baseFamilySchema, createFamilySchema } from '@/schemas/familySchema' -type TMultiSelect = { - value: string - label: string -} - -interface IFamilyForm { - title: string - summary: string - geography: string - category: string - corpus: IConfigCorpus - collections?: TMultiSelect[] - author?: string - author_type?: string - topic?: TMultiSelect[] - hazard?: TMultiSelect[] - sector?: TMultiSelect[] - keyword?: TMultiSelect[] - framework?: TMultiSelect[] - instrument?: TMultiSelect[] -} - -export type TChildEntity = 'document' | 'event' - -type TProps = { +interface FamilyFormProps { family?: TFamily } -const getCollection = (collectionId: string, collections: ICollection[]) => { - return collections.find((collection) => collection.import_id === collectionId) -} - -export const FamilyForm = ({ family: loadedFamily }: TProps) => { +export const FamilyForm: React.FC = ({ + family: loadedFamily, +}) => { const [isLeavingModalOpen, setIsLeavingModalOpen] = useState(false) const [isFormSubmitting, setIsFormSubmitting] = useState(false) const { isOpen, onOpen, onClose } = useDisclosure() @@ -123,182 +86,124 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { } = useCollections('') const toast = useToast() const [formError, setFormError] = useState() + + // Initialize corpus and taxonomy first + const initialCorpusInfo = useCorpusFromConfig( + config?.corpora, + loadedFamily?.corpus_import_id, + loadedFamily?.corpus_import_id, + ) + const initialTaxonomy = useTaxonomy( + initialCorpusInfo?.corpus_type, + initialCorpusInfo?.taxonomy, + loadedFamily?.corpus_import_id, + ) + + // Create initial validation schema + const validationSchema = useMemo(() => { + const metadataSchema = generateDynamicValidationSchema( + initialTaxonomy, + initialCorpusInfo, + ) + return createFamilySchema(metadataSchema) + }, [initialTaxonomy, initialCorpusInfo]) + const { - register, - watch, - handleSubmit, control, + handleSubmit, reset, setError, setValue, - formState: { errors, isSubmitting }, - formState: { dirtyFields }, + watch, + formState: { errors, isSubmitting, dirtyFields }, + trigger, } = useForm({ - resolver: yupResolver(familySchema), + resolver: yupResolver(validationSchema), }) - const [editingEntity, setEditingEntity] = useState() - const [editingEvent, setEditingEvent] = useState() - const [editingDocument, setEditingDocument] = useState< - IDocument | undefined - >() - const [familyDocuments, setFamilyDocuments] = useState([]) - const [familyEvents, setFamilyEvents] = useState([]) - const [updatedEvent, setUpdatedEvent] = useState('') - const [updatedDocument, setUpdatedDocument] = useState('') + // Watch for corpus changes and update schema const watchCorpus = watch('corpus') const corpusInfo = useCorpusFromConfig( config?.corpora, loadedFamily?.corpus_import_id, watchCorpus?.value, ) + const taxonomy = useTaxonomy( + corpusInfo?.corpus_type, + corpusInfo?.taxonomy, + watchCorpus?.value, + ) - console.log('loadedFamily', loadedFamily) - console.log('corpusInfo', corpusInfo) - console.log('CORPUS_METADATA_CONFIG', CORPUS_METADATA_CONFIG) - - useEffect(() => { - if (loadedFamily && corpusInfo) { - console.log('Loaded Family Metadata:', loadedFamily.metadata) - console.log('Corpus Type:', corpusInfo.corpus_type) - console.log( - 'Render Fields:', - CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields, - ) - } - }, [loadedFamily, corpusInfo]) - + // Update validation schema when corpus/taxonomy changes useEffect(() => { - if (loadedFamily) { - console.log('Full Loaded Family Metadata:', loadedFamily.metadata) - console.log('Metadata Keys:', Object.keys(loadedFamily.metadata)) - - // Detailed logging for each metadata field - const metadataFields = [ - 'topic', - 'hazard', - 'sector', - 'keyword', - 'framework', - 'instrument', - 'author', - 'author_type', - ] - - metadataFields.forEach((field) => { - console.log(`${field} exists:`, field in loadedFamily.metadata) - if (field in loadedFamily.metadata) { - console.log(`${field} value:`, loadedFamily.metadata[field]) - } - }) - } - }, [loadedFamily]) - - const corpusTitle = loadedFamily - ? loadedFamily?.corpus_title - : corpusInfo?.title + const metadataSchema = generateDynamicValidationSchema(taxonomy, corpusInfo) + const newSchema = createFamilySchema(metadataSchema) + // Re-trigger form validation with new schema + trigger() + }, [taxonomy, corpusInfo]) - const taxonomy = useTaxonomy(corpusInfo?.corpus_type, corpusInfo?.taxonomy) - console.log('taxonomy', taxonomy) + const [editingEntity, setEditingEntity] = useState() + const [editingEvent, setEditingEvent] = useState() + const [editingDocument, setEditingDocument] = useState< + IDocument | undefined + >() + const [familyDocuments, setFamilyDocuments] = useState([]) + const [familyEvents, setFamilyEvents] = useState( + loadedFamily?.events || [], + ) + const [updatedEvent, setUpdatedEvent] = useState('') + const [updatedDocument, setUpdatedDocument] = useState('') - const userToken = useMemo(() => { + const userAccess = useMemo(() => { const token = localStorage.getItem('token') - if (!token) return null - const decodedToken: IDecodedToken | null = decodeToken(token) - return decodedToken - }, []) - - const userAccess = !userToken ? null : userToken.authorisation - const isSuperUser = !userToken ? false : userToken.is_superuser - - // TODO: Get org_id from corpus PDCT-1171. - const orgName = loadedFamily ? String(loadedFamily?.organisation) : null - - const userCanModify = useMemo( - () => canModify(orgName, isSuperUser, userAccess), - [orgName, isSuperUser, userAccess], - ) + if (!token) return { canModify: false, isSuperUser: false } + const decodedToken = decodeToken(token) + return { + canModify: canModify( + loadedFamily ? String(loadedFamily.organisation) : null, + decodedToken?.is_superuser, + decodedToken?.authorisation, + ), + isSuperUser: decodedToken?.is_superuser || false, + } + }, [loadedFamily]) - // Family handlers - const handleFormSubmission = async (family: IFamilyForm) => { + const handleFormSubmission = async (formData: IFamilyForm) => { setIsFormSubmitting(true) setFormError(null) - let familyMetadata = {} as TFamilyFormPostMetadata - if (corpusInfo?.corpus_type == 'Intl. agreements') { - const metadata = familyMetadata as IUNFCCCMetadata - if (family.author) metadata.author = [family.author] - if (family.author_type) metadata.author_type = [family.author_type] - familyMetadata = metadata - } else if (corpusInfo?.corpus_type == 'Laws and Policies') { - const metadata: ICCLWMetadata = { - topic: family.topic?.map((topic) => topic.value) || [], - hazard: family.hazard?.map((hazard) => hazard.value) || [], - sector: family.sector?.map((sector) => sector.value) || [], - keyword: family.keyword?.map((keyword) => keyword.value) || [], - framework: family.framework?.map((framework) => framework.value) || [], - instrument: - family.instrument?.map((instrument) => instrument.value) || [], - } - familyMetadata = metadata - } - - // @ts-expect-error: TODO: fix this - const familyData: TFamilyFormPost = { - title: family.title, - summary: family.summary, - geography: family.geography, - category: family.category, - corpus_import_id: family.corpus?.value || '', - collections: - family.collections?.map((collection) => collection.value) || [], - metadata: familyMetadata, - } - - if (loadedFamily) { - return await updateFamily(familyData, loadedFamily.import_id) - .then(() => { - toast.closeAll() - toast({ - title: 'Family has been successfully updated', - status: 'success', - position: 'top', - }) - }) - .catch((error: IError) => { - setFormError(error) - toast({ - title: 'Family has not been updated', - description: error.message, - status: 'error', - position: 'top', - }) - }) - } + const familyMetadata = generateFamilyMetadata(formData, corpusInfo) + const familyData = generateFamilyData(formData, familyMetadata) - return await createFamily(familyData) - .then((data) => { - toast.closeAll() + try { + if (loadedFamily) { + await updateFamily(familyData, loadedFamily.import_id) toast({ - title: 'Family has been successfully created', + title: 'Family has been successfully updated', status: 'success', position: 'top', }) - navigate(`/family/${data.response}/edit`) - }) - .catch((error: IError) => { - setFormError(error) + } else { + const response = await createFamily(familyData) toast({ - title: 'Family has not been created', - description: error.message, - status: 'error', + title: 'Family has been successfully created', + status: 'success', position: 'top', }) + navigate(`/family/${response.response}/edit`) + } + } catch (error) { + setFormError(error as IError) + toast({ + title: `Family has not been ${loadedFamily ? 'updated' : 'created'}`, + description: (error as IError).message, + status: 'error', + position: 'top', }) - .finally(() => { - setIsFormSubmitting(false) - }) - } // end handleFormSubmission + } finally { + setIsFormSubmitting(false) + } + } const onSubmit: SubmitHandler = (data) => { handleFormSubmission(data).catch((error: IError) => { @@ -306,13 +211,11 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { }) } - // object type is workaround for SubmitErrorHandler throwing a tsc error. const onSubmitErrorHandler = (error: object) => { console.log('onSubmitErrorHandler', error) const submitHandlerErrors = error as { [key: string]: { message: string; type: string } } - // Set form errors manually Object.keys(submitHandlerErrors).forEach((key) => { if (key === 'summary') setError('summary', { @@ -322,7 +225,6 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { }) } - // Child entity handlers const onAddNewEntityClick = (entityType: TChildEntity) => { setEditingEntity(entityType) if (entityType === 'document') setEditingDocument(undefined) @@ -340,7 +242,6 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { onOpen() } - // Document handlers const onDocumentFormSuccess = (documentId: string) => { onClose() if (familyDocuments.includes(documentId)) @@ -386,7 +287,6 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { setValue('summary', html, { shouldDirty: true }) } - // Event handlers const onEventFormSuccess = (eventId: string) => { onClose() if (familyEvents.includes(eventId)) setFamilyEvents([...familyEvents]) @@ -397,73 +297,48 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { const canLoadForm = !configLoading && !collectionsLoading && !configError && !collectionsError + console.log('Loading tax data:', taxonomy) useEffect(() => { - if (loadedFamily) { - setFamilyDocuments(loadedFamily.documents) - setFamilyEvents(loadedFamily.events) - // set the form values to that of the loaded family + if (loadedFamily && collections) { + console.log(loadedFamily) + setFamilyDocuments(loadedFamily.documents || []) + setFamilyEvents(loadedFamily.events || []) + reset({ title: loadedFamily.title, summary: loadedFamily.summary, - collections: loadedFamily.collections.map((collectionId) => { - const collection = getCollection(collectionId, collections) - if (!collection) return null - return { - value: collection.import_id, - label: collection.title, - } - }), - geography: loadedFamily.geography, - category: loadedFamily.category, - corpus: loadedFamily.corpus_import_id + collections: + loadedFamily.collections + ?.map((collectionId) => { + const collection = collections.find( + (c) => c.import_id === collectionId, + ) + return collection + ? { + value: collection.import_id, + label: collection.title, + } + : null + }) + .filter(Boolean) || [], + geography: loadedFamily.geography ? { - label: loadedFamily.corpus_import_id, - value: loadedFamily.corpus_import_id, + value: loadedFamily.geography, + label: + getCountries(config?.geographies).find( + (country) => country.value === loadedFamily.geography, + )?.display_value || loadedFamily.geography, } : undefined, - topic: - 'topic' in loadedFamily.metadata && - Array.isArray(loadedFamily.metadata.topic) - ? generateOptions(loadedFamily.metadata.topic) - : [], - hazard: - 'hazard' in loadedFamily.metadata && - Array.isArray(loadedFamily.metadata.hazard) - ? generateOptions(loadedFamily.metadata.hazard) - : [], - sector: - 'sector' in loadedFamily.metadata && - Array.isArray(loadedFamily.metadata.sector) - ? generateOptions(loadedFamily.metadata.sector) - : [], - keyword: - 'keyword' in loadedFamily.metadata && - Array.isArray(loadedFamily.metadata.keyword) - ? generateOptions(loadedFamily.metadata.keyword) - : [], - framework: - 'framework' in loadedFamily.metadata && - Array.isArray(loadedFamily.metadata.framework) - ? generateOptions(loadedFamily.metadata.framework) - : [], - instrument: - 'instrument' in loadedFamily.metadata && - Array.isArray(loadedFamily.metadata.instrument) - ? generateOptions(loadedFamily.metadata.instrument) - : [], - author: - 'author' in loadedFamily.metadata - ? loadedFamily.metadata.author[0] - : '', - author_type: - 'author_type' in loadedFamily.metadata - ? loadedFamily.metadata.author_type[0] - : '', + category: loadedFamily.category, + corpus: { + value: loadedFamily.corpus_import_id, + label: loadedFamily.corpus_name, + }, }) } }, [loadedFamily, collections, reset]) - // Internal and external navigation blocker for unsaved changes const blocker = useBlocker( ({ currentLocation, nextLocation }) => !isFormSubmitting && @@ -495,373 +370,167 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { } }, [handleBeforeUnload]) - const renderDynamicMetadataFields = useCallback(() => { - if (!corpusInfo || !taxonomy) return null - - return ( - taxonomy && - Object.entries( - (CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type]?.renderFields || - {}) as Record, - ).map(([fieldKey, fieldConfig]) => ( - - )) - ) - }, [corpusInfo, taxonomy, control, errors]) - - const validationSchema = useMemo(() => { - return generateDynamicValidationSchema(taxonomy, corpusInfo) - }, [taxonomy, corpusInfo]) - return ( <> - {(configLoading || collectionsLoading) && ( - - - - + {!canLoadForm && ( + )} - {!userCanModify && ( + {!userAccess.canModify && ( )} - {configError && } - {collectionsError && } {(configError || collectionsError) && ( - + )} + {canLoadForm && ( - <> - {isLeavingModalOpen && ( - setIsLeavingModalOpen(false)} - > - - - Are you sure you want to leave? - - Changes that you made may not be saved. - - - - - - - )} -
- - {formError && } - {loadedFamily && ( - <> - - Import ID - - - - Corpus ID - - - - - Corpus Title - - - - Corpus Type - - - - )} - - Title - - - - Summary - - Summary is required - - { - return ( - - Collections - ({ - value: collection.import_id, - label: collection.title, - })) || [] - } - {...field} - /> - - ) - }} - /> - + + {formError && } + + {loadedFamily && } + + + + + + ({ + value: collection.import_id, + label: collection.title, + })) || [] + } + isMulti={true} + isRequired={false} + /> + + ({ + value: country.value, + label: country.display_value, + }))} + isMulti={false} + isRequired={true} + /> + + {!loadedFamily && ( + { - return ( - - Geography - - - ) - }} + options={ + config?.corpora.map((corpus) => ({ + value: corpus.corpus_import_id, + label: corpus.title, + })) || [] + } + rules={{ required: true }} /> - {!loadedFamily && ( - { - return ( - - Corpus - ({ - value: corpus.corpus_import_id, - label: corpus.title, - })) || [] - } - {...field} - /> - - ) - }} - /> - )} - + + {corpusInfo && ( + { - return ( - - Category - - - - Executive - - - Legislative - - - Litigation - - - UNFCCC - - - - MCF - - - - - Please select a category - - - ) - }} - /> - {corpusInfo !== null && ( - - - - Metadata - - - )} - {taxonomy && - Object.entries( - (CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type] - ?.renderFields || {}) as Record< - string, - { type: FieldType } - >, - ).map(([fieldKey, fieldConfig]) => ( - - ))} - - - - Documents - - - {!loadedFamily && ( - - Please create the family first before attempting to add - documents - - )} - {familyDocuments.length && ( - - {familyDocuments.map((familyDoc) => ( - onEditEntityClick('document', id)} - onDeleteClick={onDocumentDeleteClick} - updatedDocument={updatedDocument} - setUpdatedDocument={setUpdatedDocument} - /> - ))} - - )} - {loadedFamily && ( - - - - )} - - - + )} + + + + + + + + - - {editingEntity === 'document' && loadedFamily && ( - - - - )} - {editingEntity === 'event' && loadedFamily && ( - - - - )} - +
+ + )} + + setIsLeavingModalOpen(false)} + onConfirm={() => { + blocker.proceed?.() + setIsLeavingModalOpen(false) + }} + /> + + {isOpen && editingEntity && ( + )} ) diff --git a/src/components/forms/ReadOnlyFields.tsx b/src/components/forms/ReadOnlyFields.tsx new file mode 100644 index 0000000..5e8063b --- /dev/null +++ b/src/components/forms/ReadOnlyFields.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { Box, Text, VStack, Divider } from '@chakra-ui/react' +import { TFamily } from '@/interfaces' + +interface ReadOnlyFieldsProps { + family: TFamily +} + +export const ReadOnlyFields: React.FC = ({ family }) => { + return ( + <> + + + + Owner: {family.organisation} + + + + Corpus ID: {family.corpus_import_id} + + + + Corpus Name: {family.corpus_title} + + + + Corpus Type: {family.corpus_type} + + + + Created At:{' '} + {new Date(family.created).toLocaleString()} + + {family.last_modified && ( + <> + + + Last Updated:{' '} + {new Date(family.last_modified).toLocaleString()} + + + )} + + + + ) +} diff --git a/src/components/forms/fields/RadioGroupField.tsx b/src/components/forms/fields/RadioGroupField.tsx new file mode 100644 index 0000000..2f2c576 --- /dev/null +++ b/src/components/forms/fields/RadioGroupField.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import { Control, Controller, FieldValues, Path, RegisterOptions } from 'react-hook-form' +import { + FormControl, + FormLabel, + FormErrorMessage, + RadioGroup, + Radio, + HStack, +} from '@chakra-ui/react' + +interface Option { + value: string + label: string +} + +interface RadioGroupFieldProps { + name: Path + label: string + control: Control + options: Option[] + rules?: RegisterOptions +} + +export const RadioGroupField = ({ + name, + label, + control, + options, + rules, +}: RadioGroupFieldProps) => { + return ( + ( + + {label} + + + {options.map((option) => ( + + {option.label} + + ))} + + + {error && {error.message}} + + )} + /> + ) +} diff --git a/src/components/forms/fields/SelectField.tsx b/src/components/forms/fields/SelectField.tsx new file mode 100644 index 0000000..b9c114d --- /dev/null +++ b/src/components/forms/fields/SelectField.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import { Controller, Control, FieldErrors } from 'react-hook-form' +import { Select as CRSelect } from 'chakra-react-select' +import { chakraStylesSelect } from '@/styles/chakra' +import { FieldType, SelectOption } from '@/types/metadata' +import { generateSelectOptions } from '@/utils/metadataUtils' +import { FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react' + +interface SelectFieldProps> { + name: string + label?: string + control: Control + options: string[] | SelectOption[] + isMulti?: boolean + rules?: any + isRequired?: boolean +} + +export const SelectField = >({ + name, + label, + control, + options, + isMulti = false, + rules, + isRequired, +}: SelectFieldProps): React.ReactElement => { + // Determine if options are already in SelectOption format + const selectOptions = options ? + (Array.isArray(options) && options.length > 0 && + typeof options[0] === 'object' && 'value' in options[0] + ? options + : generateSelectOptions(options as string[])) + : [] + + return ( + { + if (isRequired) { + if (isMulti) { + return (value && value.length > 0) || 'This field is required' + } + return value || 'This field is required' + } + return true + } + }} + render={({ field, fieldState: { error } }) => ( + + {label && {label}} + + {error && {error.message}} + + )} + /> + ) +} diff --git a/src/components/forms/fields/TextField.tsx b/src/components/forms/fields/TextField.tsx new file mode 100644 index 0000000..06d8ddf --- /dev/null +++ b/src/components/forms/fields/TextField.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { Controller, Control } from 'react-hook-form' +import { Input, FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react' + +interface TextFieldProps> { + name: string + control: Control + type?: 'text' | 'number' + placeholder?: string + label?: string + isRequired?: boolean +} + +export const TextField = >({ + name, + control, + type = 'text', + placeholder, + label, + isRequired, +}: TextFieldProps): React.ReactElement => { + return ( + ( + + {label && {label}} + + {error && {error.message}} + + )} + /> + ) +} diff --git a/src/components/forms/fields/WYSIWYGField.tsx b/src/components/forms/fields/WYSIWYGField.tsx new file mode 100644 index 0000000..7bb0747 --- /dev/null +++ b/src/components/forms/fields/WYSIWYGField.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { Control, Controller, FieldValues, Path, RegisterOptions } from 'react-hook-form' +import { FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react' +import { WYSIWYG } from '@/components/form-components/WYSIWYG' +import { FieldError } from 'react-hook-form' + +interface WYSIWYGFieldProps { + name: Path + label: string + control: Control + defaultValue?: string + onChange: (html: string) => void + error?: FieldError + isRequired?: boolean +} + +export const WYSIWYGField = ({ + name, + label, + control, + defaultValue, + onChange, + error, + isRequired = false, +}: WYSIWYGFieldProps) => { + return ( + isRequired ? (value && value.trim() !== '') || 'This field is required' : true + }} + render={({ field }) => ( + + {label} + { + field.onChange(html) + onChange(html) + }} + /> + {error && {error.message}} + + )} + /> + ) +} diff --git a/src/components/forms/modals/UnsavedChangesModal.tsx b/src/components/forms/modals/UnsavedChangesModal.tsx new file mode 100644 index 0000000..1e5aa45 --- /dev/null +++ b/src/components/forms/modals/UnsavedChangesModal.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + Button, + ModalCloseButton, +} from '@chakra-ui/react' + +interface UnsavedChangesModalProps { + isOpen: boolean + onClose: () => void + onConfirm: () => void +} + +export const UnsavedChangesModal: React.FC = ({ + isOpen, + onClose, + onConfirm, +}) => { + return ( + + + + Are you sure you want to leave? + + Changes that you made may not be saved. + + + + + + + ) +} diff --git a/src/components/forms/sections/DocumentSection.tsx b/src/components/forms/sections/DocumentSection.tsx new file mode 100644 index 0000000..699d977 --- /dev/null +++ b/src/components/forms/sections/DocumentSection.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import { Box, Button, Divider, AbsoluteCenter, Flex, Text } from '@chakra-ui/react' +import { WarningIcon } from '@chakra-ui/icons' +import { FamilyDocument } from '@/components/family/FamilyDocument' +import { IDocument } from '@/interfaces' + +interface DocumentSectionProps { + familyDocuments: string[] + userCanModify: boolean + onAddNew: (type: 'document') => void + onEdit: (type: 'document', document: IDocument) => void + onDelete: (documentId: string) => void + updatedDocument: string + setUpdatedDocument: (id: string) => void + isNewFamily: boolean +} + +export const DocumentSection: React.FC = ({ + familyDocuments, + userCanModify, + onAddNew, + onEdit, + onDelete, + updatedDocument, + setUpdatedDocument, + isNewFamily, +}) => { + return ( + <> + + + + Documents + + + + {isNewFamily && ( + Please create the family first before attempting to add documents + )} + + {familyDocuments.length > 0 && ( + + {familyDocuments.map((documentId) => ( + onEdit('document', doc)} + onDeleteClick={onDelete} + updatedDocument={updatedDocument} + setUpdatedDocument={setUpdatedDocument} + /> + ))} + + )} + + {!isNewFamily && ( + + + + )} + + ) +} diff --git a/src/components/forms/sections/EventSection.tsx b/src/components/forms/sections/EventSection.tsx new file mode 100644 index 0000000..83ba474 --- /dev/null +++ b/src/components/forms/sections/EventSection.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { FamilyEventList } from '@/components/lists/FamilyEventList' +import { IEvent } from '@/interfaces' + +interface EventSectionProps { + familyEvents: string[] + userCanModify: boolean + onAddNew: (type: 'event') => void + onEdit: (type: 'event', event: IEvent) => void + updatedEvent: string + setUpdatedEvent: (id: string) => void + isNewFamily: boolean + onSetFamilyEvents: (events: string[]) => void +} + +export const EventSection: React.FC = ({ + familyEvents, + userCanModify, + onAddNew, + onEdit, + updatedEvent, + setUpdatedEvent, + isNewFamily, + onSetFamilyEvents, +}) => { + return ( + onEdit('event', event)} + onAddNewEntityClick={() => onAddNew('event')} + loadedFamily={!isNewFamily} + updatedEvent={updatedEvent} + setUpdatedEvent={setUpdatedEvent} + /> + ) +} diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx new file mode 100644 index 0000000..b2a5a22 --- /dev/null +++ b/src/components/forms/sections/MetadataSection.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from 'react' +import { Control, FieldErrors, UseFormReset } from 'react-hook-form' +import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' +import { DynamicMetadataField } from '../DynamicMetadataFields' +import { CORPUS_METADATA_CONFIG, FieldType } from '@/schemas/dynamicValidationSchema' +import { IConfigCorpus, TFamily } from '@/interfaces' + +interface MetadataSectionProps { + corpusInfo: IConfigCorpus + taxonomy: any + control: Control + errors: FieldErrors + loadedFamily?: TFamily + reset: UseFormReset +} + +export const MetadataSection: React.FC = ({ + corpusInfo, + taxonomy, + control, + errors, + loadedFamily, + reset, +}) => { + useEffect(() => { + if (loadedFamily?.metadata && corpusInfo) { + const metadataValues = Object.entries(loadedFamily.metadata).reduce((acc, [key, value]) => { + const fieldConfig = CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] + if (!fieldConfig) return acc + + if (fieldConfig.type === FieldType.SINGLE_SELECT || fieldConfig.type === FieldType.TEXT) { + acc[key] = value?.[0] ? { + value: value[0], + label: value[0] + } : undefined + } else if (fieldConfig.type === FieldType.MULTI_SELECT) { + acc[key] = value?.map(v => ({ + value: v, + label: v + })) || [] + } else { + acc[key] = value?.[0] || '' + } + return acc + }, {} as Record) + + reset((formValues) => ({ + ...formValues, + ...metadataValues + })) + } + }, [loadedFamily, corpusInfo, reset]) + + if (!corpusInfo || !taxonomy) return null + + return ( + <> + + + + Metadata + + + {Object.entries( + (CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields || {}) as Record< + string, + { type: FieldType } + > + ).map(([fieldKey, fieldConfig]) => ( + + ))} + + ) +} diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 3bea82c..d8e42c4 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -1,4 +1,11 @@ import * as yup from 'yup' +import { + FieldType, + Taxonomy, + CorpusInfo, + CorpusMetadataConfig, + ValidationSchema, +} from '@/types/metadata' // Enum for field types to ensure type safety and scalability export enum FieldType { @@ -78,6 +85,37 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { }, } +const getFieldValidation = ( + fieldType: FieldType, + isRequired: boolean, + fieldKey: string, +): yup.Schema => { + switch (fieldType) { + case FieldType.TEXT: + return isRequired + ? yup.string().required(`${fieldKey} is required`) + : yup.string() + case FieldType.MULTI_SELECT: + return isRequired + ? yup.array().of(yup.string()).min(1, `${fieldKey} is required`) + : yup.array().of(yup.string()) + case FieldType.SINGLE_SELECT: + return isRequired + ? yup.string().required(`${fieldKey} is required`) + : yup.string() + case FieldType.NUMBER: + return isRequired + ? yup.number().required(`${fieldKey} is required`) + : yup.number() + case FieldType.DATE: + return isRequired + ? yup.date().required(`${fieldKey} is required`) + : yup.date() + default: + return yup.string() + } +} + // Types for taxonomy and corpus info export interface TaxonomyField { allowed_values?: string[] @@ -95,57 +133,80 @@ export interface CorpusInfo { type ValidationSchema = yup.ObjectSchema -// Validation schema generation utility export const generateDynamicValidationSchema = ( - taxonomy: Taxonomy, - corpusInfo: CorpusInfo, - schema: ValidationSchema, -): ValidationSchema => { - if (!taxonomy) return schema - - const metadataValidation = CORPUS_METADATA_CONFIG[ - corpusInfo?.corpus_type - ]?.validationFields.reduce>((acc, fieldKey) => { + taxonomy: Taxonomy | null, + corpusInfo: CorpusInfo | null, +): yup.ObjectSchema => { + if (!taxonomy || !corpusInfo) { + return yup.object({}).required() + } + + const metadataFields = CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields || {} + const validationFields = CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.validationFields || [] + + const schemaShape = Object.entries(metadataFields).reduce((acc, [fieldKey, fieldConfig]) => { + // Get the field's taxonomy configuration const taxonomyField = taxonomy[fieldKey] - const renderField = - CORPUS_METADATA_CONFIG[corpusInfo?.corpus_type]?.renderFields[fieldKey] - - if (taxonomyField) { - // Determine validation based on field type and allow_blanks - switch (renderField?.type) { - case FieldType.TEXT: - acc[fieldKey] = taxonomyField.allow_blanks - ? yup.string() - : yup.string().required(`${fieldKey} is required`) - break - case FieldType.MULTI_SELECT: - acc[fieldKey] = taxonomyField.allow_blanks - ? yup.array().of(yup.string()) - : yup.array().of(yup.string()).min(1, `${fieldKey} is required`) - break - case FieldType.SINGLE_SELECT: - acc[fieldKey] = taxonomyField.allow_blanks - ? yup.string() - : yup.string().required(`${fieldKey} is required`) - break - case FieldType.NUMBER: - acc[fieldKey] = taxonomyField.allow_blanks - ? yup.number() - : yup.number().required(`${fieldKey} is required`) - break - case FieldType.DATE: - acc[fieldKey] = taxonomyField.allow_blanks - ? yup.date() - : yup.date().required(`${fieldKey} is required`) - break - default: - // Fallback for unspecified types - acc[fieldKey] = yup.string() + const isRequired = validationFields.includes(fieldKey) + + // Generate field validation based on field type and requirements + let fieldValidation: yup.Schema + switch (fieldConfig.type) { + case FieldType.MULTI_SELECT: + fieldValidation = yup.array().of( + yup.object({ + value: yup.string(), + label: yup.string(), + }) + ) + break + case FieldType.SINGLE_SELECT: + fieldValidation = yup.string() + break + case FieldType.TEXT: + fieldValidation = yup.string() + break + case FieldType.NUMBER: + fieldValidation = yup.number() + break + case FieldType.DATE: + fieldValidation = yup.date() + break + default: + fieldValidation = yup.mixed() + } + + // Add required validation if needed + if (isRequired) { + fieldValidation = fieldValidation.required(`${fieldKey} is required`) + } + + // Add allowed values validation if specified in taxonomy + if (taxonomyField?.allowed_values && !taxonomyField.allow_any) { + if (fieldConfig.type === FieldType.MULTI_SELECT) { + fieldValidation = fieldValidation.test( + 'allowed-values', + `${fieldKey} contains invalid values`, + (value) => { + if (!value) return true + return value.every((item: any) => + taxonomyField.allowed_values?.includes(item.value) + ) + } + ) + } else { + fieldValidation = fieldValidation.oneOf( + taxonomyField.allowed_values, + `${fieldKey} must be one of the allowed values` + ) } } - return acc + return { + ...acc, + [fieldKey]: fieldValidation, + } }, {}) - return schema.shape(metadataValidation) + return yup.object(schemaShape).required() } diff --git a/src/schemas/familySchema.ts b/src/schemas/familySchema.ts index 4743b2b..0e8ab88 100644 --- a/src/schemas/familySchema.ts +++ b/src/schemas/familySchema.ts @@ -1,30 +1,22 @@ import { IConfigCorpus } from '@/interfaces' import * as yup from 'yup' -export const familySchema = yup +// Base schema for core family fields +export const baseFamilySchema = yup .object({ - title: yup.string().required(), - summary: yup.string().required(), - geography: yup.string().required(), - category: yup.string().required(), + title: yup.string().required('Title is required'), + summary: yup.string().required('Summary is required'), + geography: yup.string().required('Geography is required'), + category: yup.string().required('Category is required'), corpus: yup.object({ - label: yup.string().required(), - value: yup.string().required(), + label: yup.string().required('Corpus label is required'), + value: yup.string().required('Corpus value is required'), }), collections: yup.array().optional(), - author: yup.string().when('corpus', { - is: (val: IConfigCorpus) => val.label == 'UNFCCC Submissions', - then: (schema) => schema.required(), - }), - author_type: yup.string().when('corpus', { - is: (val: IConfigCorpus) => val.label == 'UNFCCC Submissions', - then: (schema) => schema.required(), - }), - topic: yup.array().optional(), - hazard: yup.array().optional(), - sector: yup.array().optional(), - keyword: yup.array().optional(), - framework: yup.array().optional(), - instrument: yup.array().optional(), }) .required() + +// Function to merge base schema with dynamic metadata schema +export const createFamilySchema = (metadataSchema: yup.ObjectSchema) => { + return baseFamilySchema.concat(metadataSchema) +} diff --git a/src/types/metadata.ts b/src/types/metadata.ts new file mode 100644 index 0000000..221911c --- /dev/null +++ b/src/types/metadata.ts @@ -0,0 +1,53 @@ +import { Control, FieldErrors } from 'react-hook-form' +import * as yup from 'yup' + +export enum FieldType { + TEXT = 'text', + MULTI_SELECT = 'multi_select', + SINGLE_SELECT = 'single_select', + NUMBER = 'number', + DATE = 'date', +} + +export interface TaxonomyField { + allowed_values?: string[] + allow_any?: boolean + allow_blanks?: boolean +} + +export interface Taxonomy { + [key: string]: TaxonomyField +} + +export interface CorpusInfo { + corpus_type: string + title?: string +} + +export interface MetadataFieldConfig { + type: FieldType + label?: string + allowedValues?: string[] +} + +export interface CorpusMetadataConfig { + [corpusType: string]: { + renderFields: Record + validationFields: string[] + } +} + +export interface SelectOption { + value: string + label: string +} + +export interface DynamicMetadataFieldProps> { + fieldKey: string + taxonomyField: TaxonomyField + control: Control + errors: FieldErrors + fieldType: FieldType +} + +export type ValidationSchema = yup.ObjectSchema diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts new file mode 100644 index 0000000..f047021 --- /dev/null +++ b/src/utils/metadataUtils.ts @@ -0,0 +1,17 @@ +import { SelectOption } from '@/types/metadata' + +export const formatFieldLabel = (key: string): string => { + return key + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' ') +} + +export const generateSelectOptions = (values?: string[]): SelectOption[] => { + if (!values) return []; + return values.map((value) => ({ value, label: value })); +} + +export const isArrayField = (fieldType: string): boolean => { + return fieldType.includes('multi_select') +} diff --git a/src/views/family/Family.tsx b/src/views/family/Family.tsx index d0b2a2b..e740cb0 100644 --- a/src/views/family/Family.tsx +++ b/src/views/family/Family.tsx @@ -61,9 +61,6 @@ export default function Family() {
)} - {family && ( - Last updated on: {formatDateTime(family.last_modified)} - )} {canLoadForm && } From 9a49b57917b107805f07160ed85623482bde5bb5 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:48:14 +0000 Subject: [PATCH 14/88] Show text/num field values --- src/components/forms/FamilyForm.tsx | 4 ++-- src/components/forms/sections/MetadataSection.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index a39102e..42a86cb 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -427,7 +427,7 @@ export const FamilyForm: React.FC = ({ label='Geography' control={control} options={getCountries(config?.geographies).map((country) => ({ - value: country.value, + value: country.id, label: country.display_value, }))} isMulti={false} @@ -445,7 +445,7 @@ export const FamilyForm: React.FC = ({ label: corpus.title, })) || [] } - rules={{ required: true }} + isRequired={true} /> )} diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index b2a5a22..41a84b4 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -28,7 +28,7 @@ export const MetadataSection: React.FC = ({ const fieldConfig = CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] if (!fieldConfig) return acc - if (fieldConfig.type === FieldType.SINGLE_SELECT || fieldConfig.type === FieldType.TEXT) { + if (fieldConfig.type === FieldType.SINGLE_SELECT) { acc[key] = value?.[0] ? { value: value[0], label: value[0] @@ -38,6 +38,8 @@ export const MetadataSection: React.FC = ({ value: v, label: v })) || [] + } else if (fieldConfig.type === FieldType.TEXT || fieldConfig.type === FieldType.NUMBER) { + acc[key] = value?.[0] || '' } else { acc[key] = value?.[0] || '' } From 4ac876f9d555bf8b86a0f55b59f6658efee826d9 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:32:03 +0000 Subject: [PATCH 15/88] Fix document and event forms --- src/components/drawers/DocumentEditDrawer.tsx | 45 +++- src/components/drawers/EntityEditDrawer.tsx | 60 +++++ src/components/drawers/EventEditDrawer.tsx | 61 +++-- src/components/forms/DocumentForm.tsx | 190 ++++++++------ src/components/forms/DynamicMetadataField.tsx | 8 +- src/components/forms/EventForm.tsx | 233 ++++++++++-------- src/components/forms/FamilyForm.tsx | 4 + .../forms/fields/RadioGroupField.tsx | 12 +- src/components/forms/fields/SelectField.tsx | 14 +- src/components/forms/fields/TextField.tsx | 9 +- src/components/forms/fields/WYSIWYGField.tsx | 15 +- .../forms/modals/UnsavedChangesModal.tsx | 4 +- .../forms/sections/DocumentSection.tsx | 24 +- .../forms/sections/MetadataSection.tsx | 67 ++--- src/schemas/dynamicValidationSchema.ts | 129 +++++----- src/utils/formatDate.ts | 15 +- src/utils/metadataUtils.ts | 4 +- 17 files changed, 566 insertions(+), 328 deletions(-) create mode 100644 src/components/drawers/EntityEditDrawer.tsx diff --git a/src/components/drawers/DocumentEditDrawer.tsx b/src/components/drawers/DocumentEditDrawer.tsx index 8d9722b..edd9035 100644 --- a/src/components/drawers/DocumentEditDrawer.tsx +++ b/src/components/drawers/DocumentEditDrawer.tsx @@ -1,4 +1,4 @@ -import { IDocument } from '@/interfaces' +import React from 'react' import { Drawer, DrawerBody, @@ -6,30 +6,51 @@ import { DrawerHeader, DrawerOverlay, } from '@chakra-ui/react' -import { PropsWithChildren } from 'react' +import { TDocument } from '@/interfaces' +import { DocumentForm } from '../forms/DocumentForm' -type TProps = { - editingDocument?: IDocument +interface DocumentEditDrawerProps { + document?: TDocument + familyId?: string onClose: () => void isOpen: boolean + onSuccess?: (documentId: string) => void + canModify?: boolean + taxonomy?: any } -export const DocumentEditDrawer = ({ - editingDocument, +export const DocumentEditDrawer: React.FC = ({ + document, + familyId, onClose, isOpen, - children, -}: PropsWithChildren) => { + onSuccess, + canModify, + taxonomy, +}) => { + console.log('document', document) + console.log('familyId', familyId) + console.log('taxonomy', taxonomy) + return ( - {editingDocument - ? `Edit: ${editingDocument.title}` - : 'Add new Document'} + {document ? `Edit: ${document.title}` : 'Add new Document'} - {children} + + { + onSuccess?.(documentId) + onClose() + }} + /> + ) diff --git a/src/components/drawers/EntityEditDrawer.tsx b/src/components/drawers/EntityEditDrawer.tsx new file mode 100644 index 0000000..b231f9c --- /dev/null +++ b/src/components/drawers/EntityEditDrawer.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import { DocumentEditDrawer } from './DocumentEditDrawer' +import { EventEditDrawer } from './EventEditDrawer' +import { TDocument, TEvent } from '@/interfaces' + +interface EntityEditDrawerProps { + isOpen: boolean + onClose: () => void + entity: 'document' | 'event' + document?: TDocument + event?: TEvent + onDocumentSuccess?: (document: TDocument) => void + onEventSuccess?: (event: TEvent) => void + familyId?: string + taxonomy?: any + canModify?: boolean +} + +export const EntityEditDrawer: React.FC = ({ + isOpen, + onClose, + entity, + document, + event, + onDocumentSuccess, + onEventSuccess, + familyId, + taxonomy, + canModify, +}) => { + if (entity === 'document') { + return ( + + ) + } + + if (entity === 'event') { + return ( + + ) + } + + return null +} diff --git a/src/components/drawers/EventEditDrawer.tsx b/src/components/drawers/EventEditDrawer.tsx index d8181a3..4f84647 100644 --- a/src/components/drawers/EventEditDrawer.tsx +++ b/src/components/drawers/EventEditDrawer.tsx @@ -1,4 +1,4 @@ -import { formatDate } from '@/utils/formatDate' +import React from 'react' import { Drawer, DrawerBody, @@ -6,34 +6,51 @@ import { DrawerHeader, DrawerOverlay, } from '@chakra-ui/react' -import { IEvent } from '@/interfaces' -import { PropsWithChildren } from 'react' +import { TEvent } from '@/interfaces' +import { EventForm } from '../forms/EventForm' +import { formatDate } from '@/utils/formatDate' -type TProps = { - editingEvent?: IEvent +interface EventEditDrawerProps { + event?: TEvent + familyId?: string onClose: () => void isOpen: boolean + onSuccess?: (eventId: string) => void + canModify?: boolean + taxonomy?: any } -export const EventEditDrawer = ({ - editingEvent, +export const EventEditDrawer: React.FC = ({ + event: loadedEvent, + familyId, onClose, isOpen, - children, -}: PropsWithChildren) => { + onSuccess, + canModify, + taxonomy, +}) => { return ( - <> - - - - - {editingEvent - ? `Edit: ${editingEvent.event_title}, on ${formatDate(editingEvent.date)}` - : 'Add new Event'} - - {children} - - - + + + + + {loadedEvent + ? `Edit: ${loadedEvent.event_title}, on ${formatDate(loadedEvent.date)}` + : 'Add new Event'} + + + { + onSuccess?.(eventId) + onClose() + }} + /> + + + ) } diff --git a/src/components/forms/DocumentForm.tsx b/src/components/forms/DocumentForm.tsx index c1f5833..59f463d 100644 --- a/src/components/forms/DocumentForm.tsx +++ b/src/components/forms/DocumentForm.tsx @@ -48,23 +48,91 @@ type TProps = { export const DocumentForm = ({ document: loadedDocument, familyId, - canModify, + canModify = false, taxonomy, onSuccess, }: TProps) => { + console.log('DocumentForm Initialized', { + loadedDocument, + familyId, + canModify, + taxonomy, + }) + const { config, loading: configLoading, error: configError } = useConfig() const toast = useToast() const [formError, setFormError] = useState() + const { control, register, handleSubmit, reset, + setValue, formState: { errors, isSubmitting }, - } = useForm({ + } = useForm({ resolver: yupResolver(documentSchema), + defaultValues: { + family_import_id: familyId || loadedDocument?.family_import_id || '', + }, }) + + // Ensure family_import_id is always set + useEffect(() => { + console.log('Setting family_import_id', { + familyId, + loadedDocument, + currentValue: familyId || loadedDocument?.family_import_id, + }) + + if (familyId || loadedDocument?.family_import_id) { + setValue('family_import_id', familyId || loadedDocument?.family_import_id) + } + }, [familyId, loadedDocument, setValue]) + + // Initialize form with existing document data + useEffect(() => { + console.log('Initializing form with document data', { loadedDocument }) + + if (loadedDocument) { + reset({ + family_import_id: loadedDocument.family_import_id || familyId, + variant_name: loadedDocument.variant_name ?? '', + role: loadedDocument?.metadata?.role[0] ?? '', + type: loadedDocument?.metadata?.type[0] ?? '', + title: loadedDocument.title, + source_url: loadedDocument.source_url ?? '', + user_language_name: loadedDocument.user_language_name + ? { + label: loadedDocument.user_language_name, + value: loadedDocument.user_language_name, + } + : undefined, + }) + } + }, [loadedDocument, familyId, reset]) + + const invalidDocumentCreation = !loadedDocument && !familyId + const handleFormSubmission = async (formData: IDocumentFormPost) => { + console.log('Form Submission Started', { + formData, + familyId, + loadedDocument, + family_import_id: formData.family_import_id || familyId, + }) + + // Ensure family_import_id is always present + if (!formData.family_import_id && !familyId) { + toast({ + title: 'Error', + description: 'Family ID is required for document creation', + status: 'error', + position: 'top', + }) + return + } + setFormError(null) const convertToModified = ( @@ -79,7 +147,7 @@ export const DocumentForm = ({ } return { - family_import_id: data.family_import_id, + family_import_id: data.family_import_id || familyId || '', title: data.title, metadata: metadata, source_url: data.source_url || null, @@ -89,81 +157,49 @@ export const DocumentForm = ({ } const modifiedDocumentData = convertToModified(formData) + console.log('Modified Document Data', { modifiedDocumentData }) - if (loadedDocument) { - return await updateDocument( - modifiedDocumentData, - loadedDocument.import_id, - ) - .then((data) => { - toast.closeAll() - toast({ - title: 'Document has been successfully updated', - status: 'success', - position: 'top', - }) - onSuccess && onSuccess(data.response.import_id) - }) - .catch((error: IError) => { - setFormError(error) - toast({ - title: 'Document has not been updated', - description: error.message, - status: 'error', - position: 'top', - }) - }) - } - - return await createDocument(modifiedDocumentData) - .then((data) => { - toast.closeAll() + try { + if (loadedDocument) { + const updateResult = await updateDocument( + modifiedDocumentData, + loadedDocument.import_id, + ) + console.log('Update Result', updateResult) toast({ - title: 'Document has been successfully created', + title: 'Document has been successfully updated', status: 'success', position: 'top', }) - onSuccess && onSuccess(data.response) - }) - .catch((error: IError) => { - setFormError(error) + onSuccess && onSuccess(updateResult.response.import_id) + } else { + const createResult = await createDocument(modifiedDocumentData) + console.log('Create Result', createResult) toast({ - title: 'Document has not been created', - description: error.message, - status: 'error', + title: 'Document has been successfully created', + status: 'success', position: 'top', }) - }) - } // end handleFormSubmission - - const onSubmit: SubmitHandler = (data) => - handleFormSubmission(data) - - const invalidDocumentCreation = !loadedDocument && !familyId - - useEffect(() => { - // Handle both loading an existing document and creating a new one (for a given family) - if (loadedDocument) { - reset({ - family_import_id: loadedDocument.family_import_id, - variant_name: loadedDocument.variant_name ?? '', - role: loadedDocument?.metadata?.role[0] ?? '', - type: loadedDocument?.metadata?.type[0] ?? '', - title: loadedDocument.title, - source_url: loadedDocument.source_url ?? '', - user_language_name: loadedDocument.user_language_name - ? { - label: loadedDocument.user_language_name, - value: loadedDocument.user_language_name, - } - : undefined, - }) - } else if (familyId) { - reset({ - family_import_id: familyId, + onSuccess && onSuccess(createResult.response) + } + } catch (error) { + console.error('Document Submission Error', error) + setFormError(error as IError) + toast({ + title: loadedDocument + ? 'Document Update Failed' + : 'Document Creation Failed', + description: (error as IError)?.message, + status: 'error', + position: 'top', }) } - }, [loadedDocument, familyId, reset]) + } + + const onSubmit: SubmitHandler = (data) => { + console.log('onSubmit called', { data }) + return handleFormSubmission(data) + } return ( <> @@ -188,13 +224,20 @@ export const DocumentForm = ({ {formError && } Family ID - + This field is not editable + Title + Source URL @@ -202,6 +245,7 @@ export const DocumentForm = ({ {errors.source_url && errors.source_url.message} + + + + + diff --git a/src/components/forms/DynamicMetadataField.tsx b/src/components/forms/DynamicMetadataField.tsx index 0ddcc1d..d5df15d 100644 --- a/src/components/forms/DynamicMetadataField.tsx +++ b/src/components/forms/DynamicMetadataField.tsx @@ -17,7 +17,11 @@ export const DynamicMetadataField = >({ errors, fieldType, }: DynamicMetadataFieldProps): React.ReactElement => { - const { allowed_values = [], allow_any = false, allow_blanks = true } = taxonomyField + const { + allowed_values = [], + allow_any = false, + allow_blanks = true, + } = taxonomyField const renderField = () => { if (allow_any) { @@ -36,7 +40,7 @@ export const DynamicMetadataField = >({ /> ) case FieldType.NUMBER: - return name={fieldKey} control={control} type="number" /> + return name={fieldKey} control={control} type='number' /> case FieldType.TEXT: default: return name={fieldKey} control={control} /> diff --git a/src/components/forms/EventForm.tsx b/src/components/forms/EventForm.tsx index c7ad348..45ded2a 100644 --- a/src/components/forms/EventForm.tsx +++ b/src/components/forms/EventForm.tsx @@ -30,11 +30,16 @@ import { import { ApiError } from '../feedback/ApiError' import { formatDateISO } from '@/utils/formatDate' +type TaxonomyEventType = + | { event_type: string[] } + | { event_type: { allowed_values: string[] } } + | undefined + type TProps = { - familyId: string - canModify: boolean + familyId?: string + canModify?: boolean event?: IEvent - taxonomy?: IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC + taxonomy?: IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC | TaxonomyEventType onSuccess?: (eventId: string) => void } @@ -46,7 +51,7 @@ type TEventForm = { export const EventForm = ({ familyId, - canModify, + canModify = false, event: loadedEvent, taxonomy, onSuccess, @@ -59,10 +64,27 @@ export const EventForm = ({ control, reset, formState: { errors, isSubmitting }, - } = useForm({ + } = useForm({ resolver: yupResolver(eventSchema), + defaultValues: loadedEvent + ? { + event_title: loadedEvent.event_title, + date: loadedEvent.date ? formatDateISO(loadedEvent.date) : '', + event_type_value: loadedEvent.event_type_value, + } + : undefined, }) + useEffect(() => { + if (loadedEvent) { + reset({ + event_title: loadedEvent.event_title, + date: loadedEvent.date ? formatDateISO(loadedEvent.date) : '', + event_type_value: loadedEvent.event_type_value, + }) + } + }, [loadedEvent, reset]) + const handleFormSubmission = async (event: TEventForm) => { setFormError(null) @@ -75,127 +97,128 @@ export const EventForm = ({ event_type_value: event.event_type_value, } - return await updateEvent(eventPayload, loadedEvent.import_id) - .then((data) => { - toast.closeAll() - toast({ - title: 'Event has been successfully updated', - status: 'success', - position: 'top', - }) - onSuccess && onSuccess(data.response.import_id) + try { + const data = await updateEvent(eventPayload, loadedEvent.import_id) + toast({ + title: 'Event has been successfully updated', + status: 'success', + position: 'top', }) - .catch((error: IError) => { - setFormError(error) - toast({ - title: 'Event has not been updated', - description: error.message, - status: 'error', - position: 'top', - }) + onSuccess && onSuccess(data.response.import_id) + } catch (error) { + setFormError(error as IError) + toast({ + title: 'Event has not been updated', + description: (error as IError).message, + status: 'error', + position: 'top', }) - } + } + } else { + if (!familyId) { + toast({ + title: 'Error', + description: 'Family ID is required for event creation', + status: 'error', + position: 'top', + }) + return + } - const eventPayload: IEventFormPost = { - family_import_id: familyId, - event_title: event.event_title, - date: eventDateFormatted, - event_type_value: event.event_type_value, - } + const eventPayload: IEventFormPost = { + family_import_id: familyId, + event_title: event.event_title, + date: eventDateFormatted, + event_type_value: event.event_type_value, + } - return await createEvent(eventPayload) - .then((data) => { - toast.closeAll() + try { + const data = await createEvent(eventPayload) toast({ title: 'Event has been successfully created', status: 'success', position: 'top', }) onSuccess && onSuccess(data.response) - }) - .catch((error: IError) => { - setFormError(error) + } catch (error) { + setFormError(error as IError) toast({ title: 'Event has not been created', - description: error.message, + description: (error as IError).message, status: 'error', position: 'top', }) - }) - } // end handleFormSubmission + } + } + } const onSubmit: SubmitHandler = (data) => handleFormSubmission(data) - useEffect(() => { - if (loadedEvent) { - const eventDateFormatted = formatDateISO(loadedEvent.date) - - reset({ - event_title: loadedEvent.event_title, - date: eventDateFormatted, - event_type_value: loadedEvent.event_type_value, - }) - } - }, [loadedEvent, reset]) + const invalidEventCreation = !loadedEvent && !familyId return ( - <> - {!taxonomy && ( - - )} -
- - {formError && } - - Title - - - - Description - - - { - return ( - - Type - - - Please select a type for this event - - - ) - }} + + + + Event Title + - - - - - - + {errors.event_title && ( + {errors.event_title.message} + )} + + + + Date + + {errors.date && ( + {errors.date.message} + )} + + + + Event Type + + {errors.event_type_value && ( + + {errors.event_type_value.message} + + )} + + + + + +
+ ) } diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 42a86cb..808705f 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -46,6 +46,7 @@ import { DocumentSection } from './sections/DocumentSection' import { EventSection } from './sections/EventSection' import { UnsavedChangesModal } from './modals/UnsavedChangesModal' import { ReadOnlyFields } from './ReadOnlyFields' +import { EntityEditDrawer } from '../drawers/EntityEditDrawer' import { IFamilyForm, @@ -530,6 +531,9 @@ export const FamilyForm: React.FC = ({ event={editingEvent} onDocumentSuccess={onDocumentFormSuccess} onEventSuccess={onEventFormSuccess} + familyId={loadedFamily?.import_id} + taxonomy={taxonomy} + canModify={userAccess.canModify} /> )} diff --git a/src/components/forms/fields/RadioGroupField.tsx b/src/components/forms/fields/RadioGroupField.tsx index 2f2c576..2428602 100644 --- a/src/components/forms/fields/RadioGroupField.tsx +++ b/src/components/forms/fields/RadioGroupField.tsx @@ -1,5 +1,11 @@ import React from 'react' -import { Control, Controller, FieldValues, Path, RegisterOptions } from 'react-hook-form' +import { + Control, + Controller, + FieldValues, + Path, + RegisterOptions, +} from 'react-hook-form' import { FormControl, FormLabel, @@ -36,11 +42,11 @@ export const RadioGroupField = ({ rules={rules} render={({ field, fieldState: { error } }) => ( - {label} + {label} {options.map((option) => ( - + {option.label} ))} diff --git a/src/components/forms/fields/SelectField.tsx b/src/components/forms/fields/SelectField.tsx index b9c114d..a576746 100644 --- a/src/components/forms/fields/SelectField.tsx +++ b/src/components/forms/fields/SelectField.tsx @@ -26,11 +26,13 @@ export const SelectField = >({ isRequired, }: SelectFieldProps): React.ReactElement => { // Determine if options are already in SelectOption format - const selectOptions = options ? - (Array.isArray(options) && options.length > 0 && - typeof options[0] === 'object' && 'value' in options[0] - ? options - : generateSelectOptions(options as string[])) + const selectOptions = options + ? Array.isArray(options) && + options.length > 0 && + typeof options[0] === 'object' && + 'value' in options[0] + ? options + : generateSelectOptions(options as string[]) : [] return ( @@ -48,7 +50,7 @@ export const SelectField = >({ return value || 'This field is required' } return true - } + }, }} render={({ field, fieldState: { error } }) => ( diff --git a/src/components/forms/fields/TextField.tsx b/src/components/forms/fields/TextField.tsx index 06d8ddf..35cada2 100644 --- a/src/components/forms/fields/TextField.tsx +++ b/src/components/forms/fields/TextField.tsx @@ -1,6 +1,11 @@ import React from 'react' import { Controller, Control } from 'react-hook-form' -import { Input, FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react' +import { + Input, + FormControl, + FormLabel, + FormErrorMessage, +} from '@chakra-ui/react' interface TextFieldProps> { name: string @@ -28,7 +33,7 @@ export const TextField = >({ {label && {label}} diff --git a/src/components/forms/fields/WYSIWYGField.tsx b/src/components/forms/fields/WYSIWYGField.tsx index 7bb0747..05091b3 100644 --- a/src/components/forms/fields/WYSIWYGField.tsx +++ b/src/components/forms/fields/WYSIWYGField.tsx @@ -1,5 +1,11 @@ import React from 'react' -import { Control, Controller, FieldValues, Path, RegisterOptions } from 'react-hook-form' +import { + Control, + Controller, + FieldValues, + Path, + RegisterOptions, +} from 'react-hook-form' import { FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react' import { WYSIWYG } from '@/components/form-components/WYSIWYG' import { FieldError } from 'react-hook-form' @@ -27,9 +33,12 @@ export const WYSIWYGField = ({ isRequired ? (value && value.trim() !== '') || 'This field is required' : true + validate: (value) => + isRequired + ? (value && value.trim() !== '') || 'This field is required' + : true, }} render={({ field }) => ( diff --git a/src/components/forms/modals/UnsavedChangesModal.tsx b/src/components/forms/modals/UnsavedChangesModal.tsx index 1e5aa45..0c07eaa 100644 --- a/src/components/forms/modals/UnsavedChangesModal.tsx +++ b/src/components/forms/modals/UnsavedChangesModal.tsx @@ -29,10 +29,10 @@ export const UnsavedChangesModal: React.FC = ({ Changes that you made may not be saved. - - diff --git a/src/components/forms/sections/DocumentSection.tsx b/src/components/forms/sections/DocumentSection.tsx index 699d977..13ad1f2 100644 --- a/src/components/forms/sections/DocumentSection.tsx +++ b/src/components/forms/sections/DocumentSection.tsx @@ -1,5 +1,12 @@ import React from 'react' -import { Box, Button, Divider, AbsoluteCenter, Flex, Text } from '@chakra-ui/react' +import { + Box, + Button, + Divider, + AbsoluteCenter, + Flex, + Text, +} from '@chakra-ui/react' import { WarningIcon } from '@chakra-ui/icons' import { FamilyDocument } from '@/components/family/FamilyDocument' import { IDocument } from '@/interfaces' @@ -27,19 +34,21 @@ export const DocumentSection: React.FC = ({ }) => { return ( <> - + - + Documents {isNewFamily && ( - Please create the family first before attempting to add documents + + Please create the family first before attempting to add documents + )} {familyDocuments.length > 0 && ( - + {familyDocuments.map((documentId) => ( = ({ onClick={() => onAddNew('document')} rightIcon={ familyDocuments.length === 0 ? ( - + ) : undefined } > diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 41a84b4..feb4b21 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -2,7 +2,10 @@ import React, { useEffect } from 'react' import { Control, FieldErrors, UseFormReset } from 'react-hook-form' import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' import { DynamicMetadataField } from '../DynamicMetadataFields' -import { CORPUS_METADATA_CONFIG, FieldType } from '@/schemas/dynamicValidationSchema' +import { + CORPUS_METADATA_CONFIG, + FieldType, +} from '@/schemas/dynamicValidationSchema' import { IConfigCorpus, TFamily } from '@/interfaces' interface MetadataSectionProps { @@ -24,31 +27,41 @@ export const MetadataSection: React.FC = ({ }) => { useEffect(() => { if (loadedFamily?.metadata && corpusInfo) { - const metadataValues = Object.entries(loadedFamily.metadata).reduce((acc, [key, value]) => { - const fieldConfig = CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] - if (!fieldConfig) return acc + const metadataValues = Object.entries(loadedFamily.metadata).reduce( + (acc, [key, value]) => { + const fieldConfig = + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] + if (!fieldConfig) return acc - if (fieldConfig.type === FieldType.SINGLE_SELECT) { - acc[key] = value?.[0] ? { - value: value[0], - label: value[0] - } : undefined - } else if (fieldConfig.type === FieldType.MULTI_SELECT) { - acc[key] = value?.map(v => ({ - value: v, - label: v - })) || [] - } else if (fieldConfig.type === FieldType.TEXT || fieldConfig.type === FieldType.NUMBER) { - acc[key] = value?.[0] || '' - } else { - acc[key] = value?.[0] || '' - } - return acc - }, {} as Record) + if (fieldConfig.type === FieldType.SINGLE_SELECT) { + acc[key] = value?.[0] + ? { + value: value[0], + label: value[0], + } + : undefined + } else if (fieldConfig.type === FieldType.MULTI_SELECT) { + acc[key] = + value?.map((v) => ({ + value: v, + label: v, + })) || [] + } else if ( + fieldConfig.type === FieldType.TEXT || + fieldConfig.type === FieldType.NUMBER + ) { + acc[key] = value?.[0] || '' + } else { + acc[key] = value?.[0] || '' + } + return acc + }, + {} as Record, + ) reset((formValues) => ({ ...formValues, - ...metadataValues + ...metadataValues, })) } }, [loadedFamily, corpusInfo, reset]) @@ -57,17 +70,15 @@ export const MetadataSection: React.FC = ({ return ( <> - + - + Metadata {Object.entries( - (CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields || {}) as Record< - string, - { type: FieldType } - > + (CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields || + {}) as Record, ).map(([fieldKey, fieldConfig]) => ( { - // Get the field's taxonomy configuration - const taxonomyField = taxonomy[fieldKey] - const isRequired = validationFields.includes(fieldKey) - - // Generate field validation based on field type and requirements - let fieldValidation: yup.Schema - switch (fieldConfig.type) { - case FieldType.MULTI_SELECT: - fieldValidation = yup.array().of( - yup.object({ - value: yup.string(), - label: yup.string(), - }) - ) - break - case FieldType.SINGLE_SELECT: - fieldValidation = yup.string() - break - case FieldType.TEXT: - fieldValidation = yup.string() - break - case FieldType.NUMBER: - fieldValidation = yup.number() - break - case FieldType.DATE: - fieldValidation = yup.date() - break - default: - fieldValidation = yup.mixed() - } + const metadataFields = + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields || {} + const validationFields = + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.validationFields || [] + + const schemaShape = Object.entries(metadataFields).reduce( + (acc, [fieldKey, fieldConfig]) => { + // Get the field's taxonomy configuration + const taxonomyField = taxonomy[fieldKey] + const isRequired = validationFields.includes(fieldKey) + + // Generate field validation based on field type and requirements + let fieldValidation: yup.Schema + switch (fieldConfig.type) { + case FieldType.MULTI_SELECT: + fieldValidation = yup.array().of( + yup.object({ + value: yup.string(), + label: yup.string(), + }), + ) + break + case FieldType.SINGLE_SELECT: + fieldValidation = yup.string() + break + case FieldType.TEXT: + fieldValidation = yup.string() + break + case FieldType.NUMBER: + fieldValidation = yup.number() + break + case FieldType.DATE: + fieldValidation = yup.date() + break + default: + fieldValidation = yup.mixed() + } - // Add required validation if needed - if (isRequired) { - fieldValidation = fieldValidation.required(`${fieldKey} is required`) - } + // Add required validation if needed + if (isRequired) { + fieldValidation = fieldValidation.required(`${fieldKey} is required`) + } - // Add allowed values validation if specified in taxonomy - if (taxonomyField?.allowed_values && !taxonomyField.allow_any) { - if (fieldConfig.type === FieldType.MULTI_SELECT) { - fieldValidation = fieldValidation.test( - 'allowed-values', - `${fieldKey} contains invalid values`, - (value) => { - if (!value) return true - return value.every((item: any) => - taxonomyField.allowed_values?.includes(item.value) - ) - } - ) - } else { - fieldValidation = fieldValidation.oneOf( - taxonomyField.allowed_values, - `${fieldKey} must be one of the allowed values` - ) + // Add allowed values validation if specified in taxonomy + if (taxonomyField?.allowed_values && !taxonomyField.allow_any) { + if (fieldConfig.type === FieldType.MULTI_SELECT) { + fieldValidation = fieldValidation.test( + 'allowed-values', + `${fieldKey} contains invalid values`, + (value) => { + if (!value) return true + return value.every((item: any) => + taxonomyField.allowed_values?.includes(item.value), + ) + }, + ) + } else { + fieldValidation = fieldValidation.oneOf( + taxonomyField.allowed_values, + `${fieldKey} must be one of the allowed values`, + ) + } } - } - return { - ...acc, - [fieldKey]: fieldValidation, - } - }, {}) + return { + ...acc, + [fieldKey]: fieldValidation, + } + }, + {}, + ) return yup.object(schemaShape).required() } diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts index a985719..b7c3adf 100644 --- a/src/utils/formatDate.ts +++ b/src/utils/formatDate.ts @@ -9,7 +9,18 @@ export const formatDateTime = (date: string) => { return d.toLocaleString() } -export const formatDateISO = (date: string) => { - const d = new Date(date) +export const formatDateISO = ( + date: string | Date | null | undefined, +): string => { + if (!date) return '' + + const d = date instanceof Date ? date : new Date(date) + + // Check if the date is valid + if (isNaN(d.getTime())) { + console.warn(`Invalid date: ${date}`) + return '' + } + return d.toISOString().split('T')[0] } diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index f047021..6b7368a 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -8,8 +8,8 @@ export const formatFieldLabel = (key: string): string => { } export const generateSelectOptions = (values?: string[]): SelectOption[] => { - if (!values) return []; - return values.map((value) => ({ value, label: value })); + if (!values) return [] + return values.map((value) => ({ value, label: value })) } export const isArrayField = (fieldType: string): boolean => { From 7c1024b64467267c2f7e8362bbc0128f7d3ef77e Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:28:32 +0000 Subject: [PATCH 16/88] Remove console logs --- src/components/drawers/DocumentEditDrawer.tsx | 4 --- src/components/forms/DocumentForm.tsx | 30 ------------------- 2 files changed, 34 deletions(-) diff --git a/src/components/drawers/DocumentEditDrawer.tsx b/src/components/drawers/DocumentEditDrawer.tsx index edd9035..c048b3e 100644 --- a/src/components/drawers/DocumentEditDrawer.tsx +++ b/src/components/drawers/DocumentEditDrawer.tsx @@ -28,10 +28,6 @@ export const DocumentEditDrawer: React.FC = ({ canModify, taxonomy, }) => { - console.log('document', document) - console.log('familyId', familyId) - console.log('taxonomy', taxonomy) - return ( diff --git a/src/components/forms/DocumentForm.tsx b/src/components/forms/DocumentForm.tsx index 59f463d..3736bc3 100644 --- a/src/components/forms/DocumentForm.tsx +++ b/src/components/forms/DocumentForm.tsx @@ -52,13 +52,6 @@ export const DocumentForm = ({ taxonomy, onSuccess, }: TProps) => { - console.log('DocumentForm Initialized', { - loadedDocument, - familyId, - canModify, - taxonomy, - }) - const { config, loading: configLoading, error: configError } = useConfig() const toast = useToast() const [formError, setFormError] = useState() @@ -72,19 +65,10 @@ export const DocumentForm = ({ formState: { errors, isSubmitting }, } = useForm({ resolver: yupResolver(documentSchema), - defaultValues: { - family_import_id: familyId || loadedDocument?.family_import_id || '', - }, }) // Ensure family_import_id is always set useEffect(() => { - console.log('Setting family_import_id', { - familyId, - loadedDocument, - currentValue: familyId || loadedDocument?.family_import_id, - }) - if (familyId || loadedDocument?.family_import_id) { setValue('family_import_id', familyId || loadedDocument?.family_import_id) } @@ -92,8 +76,6 @@ export const DocumentForm = ({ // Initialize form with existing document data useEffect(() => { - console.log('Initializing form with document data', { loadedDocument }) - if (loadedDocument) { reset({ family_import_id: loadedDocument.family_import_id || familyId, @@ -115,13 +97,6 @@ export const DocumentForm = ({ const invalidDocumentCreation = !loadedDocument && !familyId const handleFormSubmission = async (formData: IDocumentFormPost) => { - console.log('Form Submission Started', { - formData, - familyId, - loadedDocument, - family_import_id: formData.family_import_id || familyId, - }) - // Ensure family_import_id is always present if (!formData.family_import_id && !familyId) { toast({ @@ -157,7 +132,6 @@ export const DocumentForm = ({ } const modifiedDocumentData = convertToModified(formData) - console.log('Modified Document Data', { modifiedDocumentData }) try { if (loadedDocument) { @@ -165,7 +139,6 @@ export const DocumentForm = ({ modifiedDocumentData, loadedDocument.import_id, ) - console.log('Update Result', updateResult) toast({ title: 'Document has been successfully updated', status: 'success', @@ -174,7 +147,6 @@ export const DocumentForm = ({ onSuccess && onSuccess(updateResult.response.import_id) } else { const createResult = await createDocument(modifiedDocumentData) - console.log('Create Result', createResult) toast({ title: 'Document has been successfully created', status: 'success', @@ -183,7 +155,6 @@ export const DocumentForm = ({ onSuccess && onSuccess(createResult.response) } } catch (error) { - console.error('Document Submission Error', error) setFormError(error as IError) toast({ title: loadedDocument @@ -197,7 +168,6 @@ export const DocumentForm = ({ } const onSubmit: SubmitHandler = (data) => { - console.log('onSubmit called', { data }) return handleFormSubmission(data) } From d859074c0fcb3b4134555133dd8de4b00ba1dce0 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:29:10 +0000 Subject: [PATCH 17/88] Delete file --- src/interfaces/Organisation.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/interfaces/Organisation.ts diff --git a/src/interfaces/Organisation.ts b/src/interfaces/Organisation.ts deleted file mode 100644 index 286132e..0000000 --- a/src/interfaces/Organisation.ts +++ /dev/null @@ -1 +0,0 @@ -export type TOrganisation = 'UNFCCC' | 'CCLW' From deec3fd5df117821511ca198c889e852708a9bc1 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:29:36 +0000 Subject: [PATCH 18/88] Remove Organisation type --- src/interfaces/Collection.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/interfaces/Collection.ts b/src/interfaces/Collection.ts index 003c943..0af421d 100644 --- a/src/interfaces/Collection.ts +++ b/src/interfaces/Collection.ts @@ -1,15 +1,13 @@ -import { TOrganisation } from './Organisation' - export interface ICollection { import_id: string title: string description: string families: string[] - organisation: TOrganisation + organisation: string } export interface ICollectionFormPost { title: string description?: string - organisation?: TOrganisation + organisation?: string } From f351454aefe82ffa244a83ca41da5516d0c80d3c Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:31:09 +0000 Subject: [PATCH 19/88] Rename types --- src/interfaces/Family.ts | 64 +++++++++++++++++------------------ src/tests/utilsTest/mocks.tsx | 19 ++++++----- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/interfaces/Family.ts b/src/interfaces/Family.ts index 0810a09..1366f2a 100644 --- a/src/interfaces/Family.ts +++ b/src/interfaces/Family.ts @@ -1,5 +1,18 @@ -import { TOrganisation } from './Organisation' +export interface IInternationalAgreementsMetadata { + author: string[] + author_type: string[] +} + +export interface ILawsAndPoliciesMetadata { + topic: string[] + hazard: string[] + sector: string[] + keyword: string[] + framework: string[] + instrument: string[] +} +// Read DTOs. interface IFamilyBase { import_id: string title: string @@ -13,7 +26,7 @@ interface IFamilyBase { last_updated_date: string | null documents: string[] collections: string[] - organisation: TOrganisation + organisation: string corpus_import_id: string corpus_title: string corpus_type: string // TODO TConfigType @@ -21,52 +34,37 @@ interface IFamilyBase { last_modified: string } -export interface IUNFCCCMetadata { - author: string[] - author_type: string[] +export interface IInternationalAgreementsFamily extends IFamilyBase { + metadata: IInternationalAgreementsMetadata } -export interface ICCLWMetadata { - topic: string[] - hazard: string[] - sector: string[] - keyword: string[] - framework: string[] - instrument: string[] +export interface ILawsAndPoliciesFamily extends IFamilyBase { + metadata: ILawsAndPoliciesMetadata } -export interface IUNFCCCFamily extends IFamilyBase { - metadata: IUNFCCCMetadata - organisation: 'UNFCCC' -} - -export interface ICCLWFamily extends IFamilyBase { - metadata: ICCLWMetadata - organisation: 'CCLW' -} - -export type TFamily = IUNFCCCFamily | ICCLWFamily +export type TFamily = IInternationalAgreementsFamily | ILawsAndPoliciesFamily +// DTO for Create and Write. interface IFamilyFormPostBase { title: string summary: string geography: string category: string - organisation: string - corpus_import_id: string collections: string[] + corpus_import_id: string } -export interface ICCLWFamilyFormPost extends IFamilyFormPostBase { - organisation: 'CCLW' - metadata: ICCLWMetadata +export interface ILawsAndPoliciesFamilyFormPost extends IFamilyFormPostBase { + metadata: ILawsAndPoliciesMetadata } -export interface IUNFCCCFamilyFormPost extends IFamilyFormPostBase { - organisation: 'UNFCCC' - metadata: IUNFCCCMetadata +export interface IInternationalAgreementsFamilyFormPost + extends IFamilyFormPostBase { + metadata: IInternationalAgreementsMetadata } -export type TFamilyFormPostMetadata = IUNFCCCMetadata | ICCLWMetadata +export type TFamilyFormPostMetadata = + | IInternationalAgreementsMetadata + | ILawsAndPoliciesMetadata -export type TFamilyFormPost = ICCLWFamilyFormPost | IUNFCCCFamilyFormPost +export type TFamilyFormPost = IFamilyFormPostBase diff --git a/src/tests/utilsTest/mocks.tsx b/src/tests/utilsTest/mocks.tsx index f27f161..390e8ec 100644 --- a/src/tests/utilsTest/mocks.tsx +++ b/src/tests/utilsTest/mocks.tsx @@ -1,5 +1,8 @@ import { ICollection, IConfig, IDocument, IEvent } from '@/interfaces' -import { ICCLWFamily, IUNFCCCFamily } from '@/interfaces/Family' +import { + ILawsAndPoliciesFamily, + IInternationalAgreementsFamily, +} from '@/interfaces/Family' const mockConfig = { geographies: [ @@ -36,7 +39,7 @@ const mockConfig = { }, } -const mockUNFCCCFamily: IUNFCCCFamily = { +const mockUNFCCCFamily: IInternationalAgreementsFamily = { import_id: 'UNFCCC.family.1.0', title: 'UNFCCC Family One', summary: 'Summary for UNFCCC Family One', @@ -61,7 +64,7 @@ const mockUNFCCCFamily: IUNFCCCFamily = { }, } -const mockCCLWFamily: ICCLWFamily = { +const mockCCLWFamily: ILawsAndPoliciesFamily = { import_id: 'CCLW.family.2.0', title: 'CCLW Family Two', summary: 'Summary for CCLW Family Two', @@ -90,7 +93,7 @@ const mockCCLWFamily: ICCLWFamily = { }, } -const mockUNFCCCFamilyNoDocumentsNoEvents: IUNFCCCFamily = { +const mockUNFCCCFamilyNoDocumentsNoEvents: IInternationalAgreementsFamily = { ...mockUNFCCCFamily, import_id: 'UNFCCC.family.3.0', title: 'UNFCCC Family Three', @@ -103,7 +106,7 @@ const mockUNFCCCFamilyNoDocumentsNoEvents: IUNFCCCFamily = { last_updated_date: null, } -const mockCCLWFamilyNoDocuments: ICCLWFamily = { +const mockCCLWFamilyNoDocuments: ILawsAndPoliciesFamily = { ...mockCCLWFamily, import_id: 'CCLW.family.4.0', title: 'CCLW Family Four', @@ -113,7 +116,7 @@ const mockCCLWFamilyNoDocuments: ICCLWFamily = { last_modified: new Date(2021, 1, 5).toISOString(), } -const mockCCLWFamilyNoEvents: ICCLWFamily = { +const mockCCLWFamilyNoEvents: ILawsAndPoliciesFamily = { ...mockCCLWFamily, import_id: 'CCLW.family.5.0', title: 'CCLW Family Five', @@ -125,7 +128,7 @@ const mockCCLWFamilyNoEvents: ICCLWFamily = { last_updated_date: null, } -const mockCCLWFamilyWithOneEvent: ICCLWFamily = { +const mockCCLWFamilyWithOneEvent: ILawsAndPoliciesFamily = { ...mockCCLWFamilyNoDocuments, import_id: 'CCLW.family.6.0', title: 'CCLW Family Six', @@ -135,7 +138,7 @@ const mockCCLWFamilyWithOneEvent: ICCLWFamily = { last_modified: new Date(2021, 1, 8).toISOString(), } -const mockCCLWFamilyOneDocument: ICCLWFamily = { +const mockCCLWFamilyOneDocument: ILawsAndPoliciesFamily = { ...mockCCLWFamilyNoDocuments, import_id: 'CCLW.family.7.0', title: 'CCLW Family Seven', From a8d7fabe9e23c7d59a706076bf73b6d6b85d77ab Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:31:49 +0000 Subject: [PATCH 20/88] Fix schema --- src/schemas/familySchema.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/schemas/familySchema.ts b/src/schemas/familySchema.ts index 0e8ab88..0613dda 100644 --- a/src/schemas/familySchema.ts +++ b/src/schemas/familySchema.ts @@ -1,4 +1,3 @@ -import { IConfigCorpus } from '@/interfaces' import * as yup from 'yup' // Base schema for core family fields @@ -6,11 +5,16 @@ export const baseFamilySchema = yup .object({ title: yup.string().required('Title is required'), summary: yup.string().required('Summary is required'), - geography: yup.string().required('Geography is required'), + geography: yup + .object({ + label: yup.string().required(), + value: yup.string().required(), + }) + .required('Geography is required'), category: yup.string().required('Category is required'), corpus: yup.object({ - label: yup.string().required('Corpus label is required'), - value: yup.string().required('Corpus value is required'), + label: yup.string().required(), + value: yup.string().required(), }), collections: yup.array().optional(), }) From 493af4c3c7979d5ecb2cc3b9616d928b331fc0f3 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:57:36 +0000 Subject: [PATCH 21/88] Refactor --- src/components/forms/DynamicMetadataField.tsx | 68 -------- .../forms/DynamicMetadataFields.tsx | 161 +++++++----------- 2 files changed, 62 insertions(+), 167 deletions(-) delete mode 100644 src/components/forms/DynamicMetadataField.tsx diff --git a/src/components/forms/DynamicMetadataField.tsx b/src/components/forms/DynamicMetadataField.tsx deleted file mode 100644 index d5df15d..0000000 --- a/src/components/forms/DynamicMetadataField.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react' -import { - FormControl, - FormLabel, - FormErrorMessage, - FormHelperText, -} from '@chakra-ui/react' -import { DynamicMetadataFieldProps, FieldType } from '@/types/metadata' -import { formatFieldLabel } from '@/utils/metadataUtils' -import { SelectField } from './fields/SelectField' -import { TextField } from './fields/TextField' - -export const DynamicMetadataField = >({ - fieldKey, - taxonomyField, - control, - errors, - fieldType, -}: DynamicMetadataFieldProps): React.ReactElement => { - const { - allowed_values = [], - allow_any = false, - allow_blanks = true, - } = taxonomyField - - const renderField = () => { - if (allow_any) { - return name={fieldKey} control={control} /> - } - - switch (fieldType) { - case FieldType.MULTI_SELECT: - case FieldType.SINGLE_SELECT: - return ( - - name={fieldKey} - control={control} - options={allowed_values} - isMulti={fieldType === FieldType.MULTI_SELECT} - /> - ) - case FieldType.NUMBER: - return name={fieldKey} control={control} type='number' /> - case FieldType.TEXT: - default: - return name={fieldKey} control={control} /> - } - } - - return ( - - {formatFieldLabel(fieldKey)} - {fieldType === FieldType.MULTI_SELECT && ( - - You are able to search and can select multiple options - - )} - {renderField()} - - {errors[fieldKey] && `${fieldKey} is required`} - - - ) -} diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index bc6f027..965acf7 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -1,125 +1,88 @@ +import React from 'react' import { FormControl, FormLabel, FormErrorMessage, FormHelperText, - Input, } from '@chakra-ui/react' import { Controller, Control, FieldErrors } from 'react-hook-form' -import { Select as CRSelect } from 'chakra-react-select' -import { FieldType } from '@/schemas/dynamicValidationSchema' -import React from 'react' -import { chakraStylesSelect } from '@/styles/chakra' +import { FieldType } from '@/types/metadata' +import { formatFieldLabel } from '@/utils/metadataUtils' +import { SelectField } from './fields/SelectField' +import { TextField } from './fields/TextField' // Interface for rendering dynamic metadata fields -interface DynamicMetadataFieldProps { +export interface DynamicMetadataFieldProps> { fieldKey: string taxonomyField: { allowed_values?: string[] allow_any?: boolean allow_blanks?: boolean } - control: Control - errors: FieldErrors + control: Control + errors: FieldErrors fieldType: FieldType + validationFields?: string[] } -export const DynamicMetadataField: React.FC = - React.memo(({ fieldKey, taxonomyField, control, errors, fieldType }) => { - const allowedValues = taxonomyField.allowed_values || [] - const isAllowAny = taxonomyField.allow_any - const isAllowBlanks = taxonomyField.allow_blanks - - const renderFieldByType = () => { - if (isAllowAny) { - return renderTextField() - } +export const DynamicMetadataFields = >({ + fieldKey, + taxonomyField, + control, + errors, + fieldType, + validationFields = [], +}: DynamicMetadataFieldProps): React.ReactElement => { + const { + allowed_values = [], + allow_any = false, + allow_blanks = true, + } = taxonomyField - switch (fieldType) { - case FieldType.MULTI_SELECT: - case FieldType.SINGLE_SELECT: - return renderSelectField() - case FieldType.TEXT: - return renderTextField() - case FieldType.NUMBER: - return renderNumberField() - default: - return renderTextField() - } + const renderField = () => { + if (allow_any) { + return name={fieldKey} control={control} /> } - // Utility function to generate select options - const generateOptions = (values: string[]) => - values.map((value) => ({ value, label: value })) - - const renderSelectField = () => ( - ( - + name={fieldKey} + control={control} + options={allowed_values} isMulti={fieldType === FieldType.MULTI_SELECT} - isSearchable={true} - options={generateOptions(allowedValues)} - {...field} /> - )} - /> - ) - - const renderTextField = () => ( - ( - - )} - /> - ) - - const renderNumberField = () => ( - ( - - )} - /> - ) - - // Utility function to format field labels - const formatFieldLabel = (key: string): string => { - return key - .split('_') - .map( - (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), ) - .join(' ') + case FieldType.NUMBER: + return name={fieldKey} control={control} type='number' /> + case FieldType.TEXT: + default: + return name={fieldKey} control={control} /> } + } - return ( - - {formatFieldLabel(fieldKey)} - {fieldType === FieldType.MULTI_SELECT && ( - - You are able to search and can select multiple options - - )} - {renderFieldByType()} - - {errors[fieldKey] && `${fieldKey} is required`} - - - ) - }) + return ( + + {formatFieldLabel(fieldKey)} + {fieldType === FieldType.MULTI_SELECT && ( + + You are able to search and can select multiple options + + )} + {renderField()} + + {errors[fieldKey] && `${fieldKey} is required`} + + + ) +} From 7d44bbed854106db46c89eb44df49d3ee721f709 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:34:01 +0000 Subject: [PATCH 22/88] Refactor & lint --- .../forms/DynamicMetadataFields.tsx | 8 +- src/components/forms/EventForm.tsx | 8 +- src/components/forms/FamilyForm.tsx | 212 ++++++++++-------- src/components/forms/fields/WYSIWYGField.tsx | 9 +- .../forms/sections/MetadataSection.tsx | 6 +- src/interfaces/Family.ts | 1 + src/schemas/dynamicValidationSchema.ts | 133 +---------- src/schemas/familySchema.ts | 2 +- src/types/metadata.ts | 60 ++++- src/utils/formatDate.ts | 4 +- src/views/family/Family.tsx | 1 - 11 files changed, 185 insertions(+), 259 deletions(-) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index 965acf7..0d3c166 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -5,7 +5,7 @@ import { FormErrorMessage, FormHelperText, } from '@chakra-ui/react' -import { Controller, Control, FieldErrors } from 'react-hook-form' +import { Control, FieldErrors } from 'react-hook-form' import { FieldType } from '@/types/metadata' import { formatFieldLabel } from '@/utils/metadataUtils' import { SelectField } from './fields/SelectField' @@ -67,11 +67,7 @@ export const DynamicMetadataFields = >({ {formatFieldLabel(fieldKey)} {fieldType === FieldType.MULTI_SELECT && ( diff --git a/src/components/forms/EventForm.tsx b/src/components/forms/EventForm.tsx index 45ded2a..501ce41 100644 --- a/src/components/forms/EventForm.tsx +++ b/src/components/forms/EventForm.tsx @@ -1,10 +1,6 @@ import { useEffect, useState } from 'react' -import { useForm, SubmitHandler, Controller } from 'react-hook-form' +import { useForm, SubmitHandler } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' -import { - BACK_TO_FAMILIES_ERROR_DETAIL, - NO_TAXONOMY_ERROR, -} from '@/constants/errors' import { IEvent, IEventFormPost, @@ -27,7 +23,6 @@ import { useToast, Select, } from '@chakra-ui/react' -import { ApiError } from '../feedback/ApiError' import { formatDateISO } from '@/utils/formatDate' type TaxonomyEventType = @@ -61,7 +56,6 @@ export const EventForm = ({ const { register, handleSubmit, - control, reset, formState: { errors, isSubmitting }, } = useForm({ diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 808705f..caa43a9 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -1,42 +1,23 @@ -import { useEffect, useState, useMemo, useCallback } from 'react' -import { useForm, SubmitHandler, Controller } from 'react-hook-form' +import { useEffect, useState, useMemo } from 'react' +import { useForm, SubmitHandler } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { useBlocker, useNavigate } from 'react-router-dom' import { - Box, - FormControl, - FormLabel, - HStack, - Input, VStack, - Text, Button, ButtonGroup, FormErrorMessage, useToast, SkeletonText, Divider, - AbsoluteCenter, useDisclosure, - Flex, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton, } from '@chakra-ui/react' -import { WarningIcon } from '@chakra-ui/icons' -import { familySchema } from '@/schemas/familySchema' import useCorpusFromConfig from '@/hooks/useCorpusFromConfig' import useConfig from '@/hooks/useConfig' import useTaxonomy from '@/hooks/useTaxonomy' import useCollections from '@/hooks/useCollections' -import { DynamicMetadataField } from './DynamicMetadataFields' -import { generateOptions } from '@/utils/generateOptions' import { SelectField } from './fields/SelectField' import { TextField } from './fields/TextField' import { RadioGroupField } from './fields/RadioGroupField' @@ -49,12 +30,11 @@ import { ReadOnlyFields } from './ReadOnlyFields' import { EntityEditDrawer } from '../drawers/EntityEditDrawer' import { - IFamilyForm, TFamilyFormPost, + IInternationalAgreementsMetadata, + ILawsAndPoliciesMetadata, TFamilyFormPostMetadata, - IUNFCCCMetadata, - ICCLWMetadata, -} from '@/types/metadata' +} from '@/interfaces/Family' import { TChildEntity } from '@/types/entities' import { canModify } from '@/utils/canModify' import { getCountries } from '@/utils/extractNestedGeographyData' @@ -67,6 +47,7 @@ import { import { createFamily, updateFamily } from '@/api/Families' import { deleteDocument } from '@/api/Documents' import { baseFamilySchema, createFamilySchema } from '@/schemas/familySchema' +import { ApiError } from '../feedback/ApiError' interface FamilyFormProps { family?: TFamily @@ -111,6 +92,7 @@ export const FamilyForm: React.FC = ({ const { control, + register, handleSubmit, reset, setError, @@ -118,7 +100,7 @@ export const FamilyForm: React.FC = ({ watch, formState: { errors, isSubmitting, dirtyFields }, trigger, - } = useForm({ + } = useForm({ resolver: yupResolver(validationSchema), }) @@ -143,6 +125,14 @@ export const FamilyForm: React.FC = ({ trigger() }, [taxonomy, corpusInfo]) + // Determine if the corpus is an MCF type + const isMCFCorpus = useMemo(() => { + return ( + watchCorpus?.value?.startsWith('MCF') || + loadedFamily?.corpus_import_id?.startsWith('MCF') + ) + }, [watchCorpus?.value, loadedFamily?.corpus_import_id]) + const [editingEntity, setEditingEntity] = useState() const [editingEvent, setEditingEvent] = useState() const [editingDocument, setEditingDocument] = useState< @@ -169,29 +159,63 @@ export const FamilyForm: React.FC = ({ } }, [loadedFamily]) - const handleFormSubmission = async (formData: IFamilyForm) => { + const handleFormSubmission = async (formData: IFamilyFormPost) => { setIsFormSubmitting(true) setFormError(null) - const familyMetadata = generateFamilyMetadata(formData, corpusInfo) - const familyData = generateFamilyData(formData, familyMetadata) + // Dynamically generate metadata based on corpus type + const familyMetadata = {} as TFamilyFormPostMetadata + + // Handle International Agreements metadata + if (corpusInfo?.corpus_type === 'Intl. agreements') { + const intlAgreementsMetadata: IInternationalAgreementsMetadata = { + author: formData.author ? [formData.author] : [], + author_type: formData.author_type ? [formData.author_type] : [], + } + Object.assign(familyMetadata, intlAgreementsMetadata) + } + + // Handle Laws and Policies metadata + else if (corpusInfo?.corpus_type === 'Laws and Policies') { + const lawsPoliciesMetadata: ILawsAndPoliciesMetadata = { + topic: formData.topic?.map((topic) => topic.value) || [], + hazard: formData.hazard?.map((hazard) => hazard.value) || [], + sector: formData.sector?.map((sector) => sector.value) || [], + keyword: formData.keyword?.map((keyword) => keyword.value) || [], + framework: + formData.framework?.map((framework) => framework.value) || [], + instrument: + formData.instrument?.map((instrument) => instrument.value) || [], + } + Object.assign(familyMetadata, lawsPoliciesMetadata) + } + + // Prepare submission data + const submissionData: TFamilyFormPost = { + title: formData.title, + summary: formData.summary, + geography: formData.geography?.value || '', + category: isMCFCorpus ? 'MCF' : formData.category, + corpus_import_id: formData.corpus?.value || '', + collections: + formData.collections?.map((collection) => collection.value) || [], + metadata: familyMetadata, + } try { if (loadedFamily) { - await updateFamily(familyData, loadedFamily.import_id) + await updateFamily(submissionData, loadedFamily.import_id) toast({ title: 'Family has been successfully updated', status: 'success', - position: 'top', }) } else { - const response = await createFamily(familyData) + const createResult = await createFamily(submissionData) toast({ title: 'Family has been successfully created', status: 'success', - position: 'top', }) - navigate(`/family/${response.response}/edit`) + navigate(`/family/${createResult.response}/edit`) } } catch (error) { setFormError(error as IError) @@ -199,31 +223,22 @@ export const FamilyForm: React.FC = ({ title: `Family has not been ${loadedFamily ? 'updated' : 'created'}`, description: (error as IError).message, status: 'error', - position: 'top', }) } finally { setIsFormSubmitting(false) } } - const onSubmit: SubmitHandler = (data) => { - handleFormSubmission(data).catch((error: IError) => { - console.error(error) - }) + const onSubmit: SubmitHandler = async (data) => { + try { + await handleFormSubmission(data) + } catch (error) { + console.log('onSubmitErrorHandler', error) + } } const onSubmitErrorHandler = (error: object) => { console.log('onSubmitErrorHandler', error) - const submitHandlerErrors = error as { - [key: string]: { message: string; type: string } - } - Object.keys(submitHandlerErrors).forEach((key) => { - if (key === 'summary') - setError('summary', { - type: 'required', - message: 'Summary is required', - }) - }) } const onAddNewEntityClick = (entityType: TChildEntity) => { @@ -255,14 +270,12 @@ export const FamilyForm: React.FC = ({ toast({ title: 'Document deletion in progress', status: 'info', - position: 'top', }) await deleteDocument(documentId) .then(() => { toast({ title: 'Document has been successful deleted', status: 'success', - position: 'top', }) const index = familyDocuments.indexOf(documentId) if (index > -1) { @@ -276,7 +289,6 @@ export const FamilyForm: React.FC = ({ title: 'Document has not been deleted', description: error.message, status: 'error', - position: 'top', }) }) } @@ -298,14 +310,12 @@ export const FamilyForm: React.FC = ({ const canLoadForm = !configLoading && !collectionsLoading && !configError && !collectionsError - console.log('Loading tax data:', taxonomy) useEffect(() => { if (loadedFamily && collections) { - console.log(loadedFamily) setFamilyDocuments(loadedFamily.documents || []) setFamilyEvents(loadedFamily.events || []) - reset({ + const resetValues: Partial = { title: loadedFamily.title, summary: loadedFamily.summary, collections: @@ -331,14 +341,24 @@ export const FamilyForm: React.FC = ({ )?.display_value || loadedFamily.geography, } : undefined, - category: loadedFamily.category, - corpus: { - value: loadedFamily.corpus_import_id, - label: loadedFamily.corpus_name, - }, - }) + corpus: loadedFamily.corpus_import_id + ? { + label: loadedFamily.corpus_import_id, + value: loadedFamily.corpus_import_id, + } + : undefined, + } + + // Set category to MCF for MCF corpora + if (isMCFCorpus) { + resetValues.category = 'MCF' + } else { + resetValues.category = loadedFamily.category + } + + reset(resetValues) } - }, [loadedFamily, collections, reset]) + }, [loadedFamily, collections, reset, isMCFCorpus]) const blocker = useBlocker( ({ currentLocation, nextLocation }) => @@ -347,17 +367,6 @@ export const FamilyForm: React.FC = ({ currentLocation.pathname !== nextLocation.pathname, ) - const handleBeforeUnload = useCallback( - (event: BeforeUnloadEvent) => { - if (Object.keys(dirtyFields).length > 0 && !isFormSubmitting) { - event.preventDefault() - event.returnValue = - 'Are you sure you want leave? Changes that you made may not be saved.' - } - }, - [dirtyFields, isFormSubmitting], - ) - useEffect(() => { if (blocker && blocker.state === 'blocked') { setIsLeavingModalOpen(true) @@ -365,11 +374,10 @@ export const FamilyForm: React.FC = ({ }, [blocker]) useEffect(() => { - window.addEventListener('beforeunload', handleBeforeUnload) return () => { - window.removeEventListener('beforeunload', handleBeforeUnload) + window.removeEventListener('beforeunload', () => {}) } - }, [handleBeforeUnload]) + }, []) return ( <> @@ -450,32 +458,38 @@ export const FamilyForm: React.FC = ({ /> )} - - - {corpusInfo && ( - - )} + ) : null} - + {corpusInfo && ( + <> + + + + )} = ({ (CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields || {}) as Record, ).map(([fieldKey, fieldConfig]) => ( - { - switch (fieldType) { - case FieldType.TEXT: - return isRequired - ? yup.string().required(`${fieldKey} is required`) - : yup.string() - case FieldType.MULTI_SELECT: - return isRequired - ? yup.array().of(yup.string()).min(1, `${fieldKey} is required`) - : yup.array().of(yup.string()) - case FieldType.SINGLE_SELECT: - return isRequired - ? yup.string().required(`${fieldKey} is required`) - : yup.string() - case FieldType.NUMBER: - return isRequired - ? yup.number().required(`${fieldKey} is required`) - : yup.number() - case FieldType.DATE: - return isRequired - ? yup.date().required(`${fieldKey} is required`) - : yup.date() - default: - return yup.string() - } -} - -// Types for taxonomy and corpus info -export interface TaxonomyField { - allowed_values?: string[] - allow_any?: boolean - allow_blanks?: boolean -} - -export interface Taxonomy { - [key: string]: TaxonomyField -} - -export interface CorpusInfo { - corpus_type: string -} - -type ValidationSchema = yup.ObjectSchema - export const generateDynamicValidationSchema = ( taxonomy: Taxonomy | null, corpusInfo: CorpusInfo | null, @@ -150,7 +23,9 @@ export const generateDynamicValidationSchema = ( (acc, [fieldKey, fieldConfig]) => { // Get the field's taxonomy configuration const taxonomyField = taxonomy[fieldKey] - const isRequired = validationFields.includes(fieldKey) + const isRequired = + validationFields.includes(fieldKey) && + (!taxonomyField || taxonomyField.allow_blanks === false) // Generate field validation based on field type and requirements let fieldValidation: yup.Schema diff --git a/src/schemas/familySchema.ts b/src/schemas/familySchema.ts index 0613dda..353caba 100644 --- a/src/schemas/familySchema.ts +++ b/src/schemas/familySchema.ts @@ -1,6 +1,6 @@ import * as yup from 'yup' -// Base schema for core family fields +// Base schema for core family fields (non-metadata) export const baseFamilySchema = yup .object({ title: yup.string().required('Title is required'), diff --git a/src/types/metadata.ts b/src/types/metadata.ts index 221911c..fc55c3f 100644 --- a/src/types/metadata.ts +++ b/src/types/metadata.ts @@ -1,5 +1,4 @@ import { Control, FieldErrors } from 'react-hook-form' -import * as yup from 'yup' export enum FieldType { TEXT = 'text', @@ -9,6 +8,7 @@ export enum FieldType { DATE = 'date', } +// Types for taxonomy and corpus info export interface TaxonomyField { allowed_values?: string[] allow_any?: boolean @@ -21,7 +21,7 @@ export interface Taxonomy { export interface CorpusInfo { corpus_type: string - title?: string + title?: string // TODO Do we need this? } export interface MetadataFieldConfig { @@ -30,6 +30,7 @@ export interface MetadataFieldConfig { allowedValues?: string[] } +// Enhanced configuration type for corpus metadata export interface CorpusMetadataConfig { [corpusType: string]: { renderFields: Record @@ -50,4 +51,57 @@ export interface DynamicMetadataFieldProps> { fieldType: FieldType } -export type ValidationSchema = yup.ObjectSchema +// Centralised configuration for corpus metadata +export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { + 'Intl. agreements': { + renderFields: { + author: { type: FieldType.TEXT }, + author_type: { type: FieldType.SINGLE_SELECT }, + }, + validationFields: ['author', 'author_type'], + }, + 'Laws and Policies': { + renderFields: { + topic: { type: FieldType.MULTI_SELECT }, + hazard: { type: FieldType.MULTI_SELECT }, + sector: { type: FieldType.MULTI_SELECT }, + keyword: { type: FieldType.MULTI_SELECT }, + framework: { type: FieldType.MULTI_SELECT }, + instrument: { type: FieldType.MULTI_SELECT }, + }, + validationFields: [ + 'topic', + 'hazard', + 'sector', + 'keyword', + 'framework', + 'instrument', + ], + }, + AF: { + renderFields: { + region: { type: FieldType.MULTI_SELECT }, + sector: { type: FieldType.MULTI_SELECT }, + implementing_agency: { type: FieldType.MULTI_SELECT }, + status: { type: FieldType.SINGLE_SELECT }, + project_id: { type: FieldType.TEXT }, + project_url: { type: FieldType.TEXT }, + project_value_co_financing: { type: FieldType.NUMBER }, + project_value_fund_spend: { type: FieldType.NUMBER }, + }, + validationFields: [ + 'project_id', + 'project_url', + 'region', + 'sector', + 'status', + 'implementing_agency', + 'project_value_co_financing', + 'project_value_fund_spend', + ], + }, + default: { + renderFields: {}, + validationFields: [], + }, +} diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts index b7c3adf..4df8535 100644 --- a/src/utils/formatDate.ts +++ b/src/utils/formatDate.ts @@ -14,11 +14,11 @@ export const formatDateISO = ( ): string => { if (!date) return '' - const d = date instanceof Date ? date : new Date(date) + const d = date instanceof Date ? date : new Date(String(date)) // Check if the date is valid if (isNaN(d.getTime())) { - console.warn(`Invalid date: ${date}`) + console.warn(`Invalid date: ${String(date)}`) return '' } diff --git a/src/views/family/Family.tsx b/src/views/family/Family.tsx index e740cb0..2cd1bd8 100644 --- a/src/views/family/Family.tsx +++ b/src/views/family/Family.tsx @@ -12,7 +12,6 @@ import { FamilyForm } from '@/components/forms/FamilyForm' import useFamily from '@/hooks/useFamily' import { Loader } from '@/components/Loader' import { ApiError } from '@/components/feedback/ApiError' -import { formatDateTime } from '@/utils/formatDate' export default function Family() { const { importId } = useParams() From 7d521de6091a8aa2a96fbdb5612c6eae2c44a1fe Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:50:59 +0000 Subject: [PATCH 23/88] Fix imports & make summary required field --- src/components/forms/FamilyForm.tsx | 1 + src/components/forms/sections/MetadataSection.tsx | 5 +---- src/hooks/useTaxonomy.ts | 2 ++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index caa43a9..42ee24c 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -415,6 +415,7 @@ export const FamilyForm: React.FC = ({ defaultValue={loadedFamily?.summary} onChange={summaryOnChange} error={errors.summary} + isRequired={true} /> Date: Thu, 5 Dec 2024 14:20:43 +0000 Subject: [PATCH 24/88] Fix required metadata fields --- src/components/forms/DocumentForm.tsx | 10 +++++++--- src/components/forms/DynamicMetadataFields.tsx | 11 ++++------- src/components/forms/FamilyForm.tsx | 2 +- src/components/forms/sections/MetadataSection.tsx | 5 ++++- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/forms/DocumentForm.tsx b/src/components/forms/DocumentForm.tsx index 3736bc3..d0b70b6 100644 --- a/src/components/forms/DocumentForm.tsx +++ b/src/components/forms/DocumentForm.tsx @@ -74,14 +74,18 @@ export const DocumentForm = ({ } }, [familyId, loadedDocument, setValue]) - // Initialize form with existing document data + // Initialise form with existing document data useEffect(() => { if (loadedDocument) { reset({ family_import_id: loadedDocument.family_import_id || familyId, variant_name: loadedDocument.variant_name ?? '', - role: loadedDocument?.metadata?.role[0] ?? '', - type: loadedDocument?.metadata?.type[0] ?? '', + role: loadedDocument?.metadata?.role + ? loadedDocument?.metadata?.role[0] + : '', + type: loadedDocument?.metadata?.type + ? loadedDocument?.metadata?.type[0] + : '', title: loadedDocument.title, source_url: loadedDocument.source_url ?? '', user_language_name: loadedDocument.user_language_name diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index 0d3c166..b3a49d5 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -22,7 +22,6 @@ export interface DynamicMetadataFieldProps> { control: Control errors: FieldErrors fieldType: FieldType - validationFields?: string[] } export const DynamicMetadataFields = >({ @@ -31,7 +30,6 @@ export const DynamicMetadataFields = >({ control, errors, fieldType, - validationFields = [], }: DynamicMetadataFieldProps): React.ReactElement => { const { allowed_values = [], @@ -63,12 +61,11 @@ export const DynamicMetadataFields = >({ } } + // Explicitly log the requirement logic + const isRequired = !allow_blanks + return ( - + {formatFieldLabel(fieldKey)} {fieldType === FieldType.MULTI_SELECT && ( diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 42ee24c..4448bc6 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -69,7 +69,7 @@ export const FamilyForm: React.FC = ({ const toast = useToast() const [formError, setFormError] = useState() - // Initialize corpus and taxonomy first + // Initialise corpus and taxonomy first const initialCorpusInfo = useCorpusFromConfig( config?.corpora, loadedFamily?.corpus_import_id, diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 3b96e4a..95c9cf8 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -80,7 +80,10 @@ export const MetadataSection: React.FC = ({ Date: Thu, 5 Dec 2024 15:27:03 +0000 Subject: [PATCH 25/88] Fix tests for event section --- src/components/lists/FamilyEventList.tsx | 117 ------------- .../forms/sections/EventSection.test.tsx | 160 ++++++++++++++++++ .../components/lists/FamilyEventList.test.tsx | 44 ----- 3 files changed, 160 insertions(+), 161 deletions(-) delete mode 100644 src/components/lists/FamilyEventList.tsx create mode 100644 src/tests/components/forms/sections/EventSection.test.tsx delete mode 100644 src/tests/components/lists/FamilyEventList.test.tsx diff --git a/src/components/lists/FamilyEventList.tsx b/src/components/lists/FamilyEventList.tsx deleted file mode 100644 index dfac31c..0000000 --- a/src/components/lists/FamilyEventList.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { - AbsoluteCenter, - Box, - Divider, - Flex, - useToast, - Text, - Button, -} from '@chakra-ui/react' -import { FamilyEvent } from '../family/FamilyEvent' -import { IError, IEvent, TFamily } from '@/interfaces' -import { TChildEntity } from '../forms/FamilyForm' -import { deleteEvent } from '@/api/Events' -import { WarningIcon } from '@chakra-ui/icons' - -type TProps = { - familyEvents: string[] - canModify: boolean - onEditEntityClick: (entityType: TChildEntity, entityId: IEvent) => void - onAddNewEntityClick: (entityType: TChildEntity) => void - setFamilyEvents: (events: string[]) => void - loadedFamily?: TFamily - updatedEvent: string - setUpdatedEvent: (updateEvent: string) => void -} - -export const FamilyEventList = ({ - familyEvents, - canModify, - onEditEntityClick, - onAddNewEntityClick, - setFamilyEvents, - loadedFamily, - updatedEvent, - setUpdatedEvent, -}: TProps) => { - const toast = useToast() - - const onEventDeleteClick = async (eventId: string) => { - toast({ - title: 'Event deletion in progress', - status: 'info', - position: 'top', - }) - await deleteEvent(eventId) - .then(() => { - toast({ - title: 'Event has been successfully deleted', - status: 'success', - position: 'top', - }) - const index = familyEvents.indexOf(eventId) - if (index > -1) { - const newEvents = [...familyEvents] - newEvents.splice(index, 1) - setFamilyEvents(newEvents) - } - }) - .catch((error: IError) => { - toast({ - title: 'Event has not been deleted', - description: error.message, - status: 'error', - position: 'top', - }) - }) - } - - return ( - <> - - - - Events - - - {!loadedFamily && ( - - Please create the family first before attempting to add events - - )} - {familyEvents.length > 0 && ( - - {familyEvents.map((familyEvent) => ( - onEditEntityClick('event', event)} - onDeleteClick={onEventDeleteClick} - updatedEvent={updatedEvent} - setUpdatedEvent={setUpdatedEvent} - /> - ))} - - )} - {loadedFamily && ( - - - - )} - - ) -} diff --git a/src/tests/components/forms/sections/EventSection.test.tsx b/src/tests/components/forms/sections/EventSection.test.tsx new file mode 100644 index 0000000..215046b --- /dev/null +++ b/src/tests/components/forms/sections/EventSection.test.tsx @@ -0,0 +1,160 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { vi } from 'vitest' +import { mockEvent, mockFamiliesData } from '@/tests/utilsTest/mocks' +import { EventSection } from '@/components/forms/sections/EventSection' +import { formatDate } from '@/utils/formatDate' +import { deleteEvent } from '@/api/Events' + +// Mock the API +vi.mock('@/api/Events', () => ({ + deleteEvent: vi.fn(), +})) + +// Mock the useEvent hook +vi.mock('@/hooks/useEvent', () => ({ + default: vi.fn().mockReturnValue({ + event: { + date: mockEvent.date, + event_title: 'Test event title', + event_type_value: 'Appealed', + }, + error: null, + loading: false, + }), +})) + +// Mock the FamilyEvent component +vi.mock('@/components/family/FamilyEvent', () => ({ + FamilyEvent: ({ eventId, onEditClick, onDeleteClick }: any) => ( +
+
Test event title
+
Type: Appealed
+
Date: {formatDate(mockEvent.date)}
+ + +
+ ), +})) + +describe('EventSection', () => { + const mockOnAddNew = vi.fn() + const mockOnEdit = vi.fn() + const mockOnDelete = vi.fn() + const mockSetUpdatedEvent = vi.fn() + const mockSetFamilyEvents = vi.fn() + + const defaultProps = { + familyEvents: ['test-event-1', 'test-event-2'], + userCanModify: true, + onAddNew: mockOnAddNew, + onEdit: mockOnEdit, + onDelete: mockOnDelete, + updatedEvent: '', + setUpdatedEvent: mockSetUpdatedEvent, + isNewFamily: false, + } + + beforeEach(() => { + vi.clearAllMocks() + ;(deleteEvent as jest.Mock).mockClear() + }) + + it('renders existing family events', () => { + render() + + const formattedDate = formatDate(mockEvent.date) + + expect(screen.getByText('Events')).toBeInTheDocument() + expect(screen.getByText('Test event title')).toBeInTheDocument() + expect(screen.getByText('Type: Appealed')).toBeInTheDocument() + expect(screen.getByText(`Date: ${formattedDate}`)).toBeInTheDocument() + expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2) + }) + + it('renders add event button for existing family', () => { + render() + + expect(screen.getByText('Add new Event')).toBeInTheDocument() + }) + + it('does not render add event button for new family', () => { + render() + + expect(screen.getByText('Please create the family first before attempting to add events')).toBeInTheDocument() + expect(screen.queryByText('Add new Event')).not.toBeInTheDocument() + }) + + it('shows warning icon when no events exist', () => { + render() + + const addButton = screen.getByText('Add new Event') + expect(addButton).toHaveAttribute('data-test-id', 'warning-icon-event') + }) + + it('calls onAddNew when add event button is clicked', () => { + render() + + fireEvent.click(screen.getByText('Add new Event')) + expect(mockOnAddNew).toHaveBeenCalledWith('event') + }) + + it('calls onEdit when edit button is clicked', () => { + render() + + const editButtons = screen.getAllByText('Edit') + fireEvent.click(editButtons[0]) + expect(mockOnEdit).toHaveBeenCalledWith('event', mockEvent) + }) + + it('calls onDelete when delete button is clicked', async () => { + // Mock successful deletion + ;(deleteEvent as jest.Mock).mockResolvedValue({}) + + const familyEvents = ['test-event-1', 'test-event-2'] + const props = { + ...defaultProps, + familyEvents, + setFamilyEvents: mockSetFamilyEvents, + } + + render() + + const deleteButtons = screen.getAllByText('Delete') + fireEvent.click(deleteButtons[0]) + + await waitFor(() => { + expect(deleteEvent).toHaveBeenCalledWith('test-event-1') + expect(mockSetFamilyEvents).toHaveBeenCalledWith(['test-event-2']) + }) + }) + + it('handles delete error', async () => { + // Mock failed deletion + const mockError = new Error('Deletion failed') + ;(deleteEvent as jest.Mock).mockRejectedValue(mockError) + + const familyEvents = ['test-event-1', 'test-event-2'] + const props = { + ...defaultProps, + familyEvents, + setFamilyEvents: mockSetFamilyEvents, + } + + render() + + const deleteButtons = screen.getAllByText('Delete') + fireEvent.click(deleteButtons[0]) + + await waitFor(() => { + expect(deleteEvent).toHaveBeenCalledWith('test-event-1') + expect(mockSetFamilyEvents).not.toHaveBeenCalled() + }) + }) + + it('disables add event button when user cannot modify', () => { + render() + + const addButton = screen.getByText('Add new Event') + expect(addButton).toBeDisabled() + }) +}) diff --git a/src/tests/components/lists/FamilyEventList.test.tsx b/src/tests/components/lists/FamilyEventList.test.tsx deleted file mode 100644 index 6c2f9dc..0000000 --- a/src/tests/components/lists/FamilyEventList.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { mockEvent, mockFamiliesData } from '@/tests/utilsTest/mocks' - -import { FamilyEventList } from '@/components/lists/FamilyEventList' -import { formatDate } from '@/utils/formatDate' - -vi.mock('@/hooks/useEvent', () => ({ - default: vi.fn().mockReturnValue({ - event: { - date: mockEvent.date, - event_title: 'Test event title', - event_type_value: 'Appealed', - }, - error: null, - loading: false, - }), -})) - -describe('FamilyEventList', () => { - it('renders existing family events', () => { - render( - {}} - onAddNewEntityClick={() => {}} - setFamilyEvents={() => {}} - loadedFamily={mockFamiliesData[0]} - updatedEvent={''} - setUpdatedEvent={() => {}} - />, - ) - - // We put the formattedDate here so that the formatting runs in the same locale - // as the test suite component when it runs formatDate. - const formattedDate = formatDate(mockEvent.date) - - expect(screen.getByText('Events')).toBeInTheDocument() - expect(screen.getByText('Test event title')).toBeInTheDocument() - expect(screen.getByText('Type: Appealed')).toBeInTheDocument() - expect(screen.getByText(`Date: ${formattedDate}`)).toBeInTheDocument() - expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2) - }) -}) From 9a46095b379e238e7e3879e9a1c234a96cfcfe74 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:29:57 +0000 Subject: [PATCH 26/88] Fix tests --- .../drawers/EventEditDrawer.test.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/tests/components/drawers/EventEditDrawer.test.tsx b/src/tests/components/drawers/EventEditDrawer.test.tsx index 21167fb..d514011 100644 --- a/src/tests/components/drawers/EventEditDrawer.test.tsx +++ b/src/tests/components/drawers/EventEditDrawer.test.tsx @@ -1,7 +1,6 @@ import '@testing-library/jest-dom' import { render, screen } from '@testing-library/react' import { EventEditDrawer } from '@/components/drawers/EventEditDrawer' -import { EventForm } from '@/components/forms/EventForm' import { formatDate } from '@/utils/formatDate' describe('EventEditDrawer', () => { @@ -21,17 +20,12 @@ describe('EventEditDrawer', () => { } render( {}} isOpen={true} - > - {}} - /> - , + canModify={true} + familyId={'1'} + />, ) expect( screen.getByText( @@ -45,13 +39,17 @@ describe('EventEditDrawer', () => { it('renders create new event form if an editingEvent is not passed in', () => { render( - {}} isOpen={true}> - {}} /> - , + {}} + isOpen={true} + familyId={'1'} + canModify={true} + onSuccess={() => {}} + />, ) expect(screen.getByText('Add new Event')).toBeInTheDocument() expect( - screen.getByRole('button', { name: 'Create new Event' }), + screen.getByRole('button', { name: 'Create New Event' }), ).toBeInTheDocument() }) }) From bfbb6f24f6546faf64291e7b73b3fa2892be82dd Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:55:30 +0000 Subject: [PATCH 27/88] With all the type errors --- src/components/forms/EventForm.tsx | 1 + src/components/forms/FamilyForm.tsx | 39 +++++-- src/components/forms/fields/SelectField.tsx | 4 +- .../forms/sections/EventSection.tsx | 74 ++++++++++--- .../forms/sections/MetadataSection.tsx | 10 +- .../forms/sections/EventSection.test.tsx | 102 +++--------------- 6 files changed, 116 insertions(+), 114 deletions(-) diff --git a/src/components/forms/EventForm.tsx b/src/components/forms/EventForm.tsx index 501ce41..7b69e71 100644 --- a/src/components/forms/EventForm.tsx +++ b/src/components/forms/EventForm.tsx @@ -69,6 +69,7 @@ export const EventForm = ({ : undefined, }) + // Preload form with event data when loadedEvent changes useEffect(() => { if (loadedEvent) { reset({ diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 4448bc6..5d2c04a 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -6,7 +6,6 @@ import { VStack, Button, ButtonGroup, - FormErrorMessage, useToast, SkeletonText, Divider, @@ -40,13 +39,11 @@ import { canModify } from '@/utils/canModify' import { getCountries } from '@/utils/extractNestedGeographyData' import { decodeToken } from '@/utils/decodeToken' import { stripHtml } from '@/utils/stripHtml' -import { - CORPUS_METADATA_CONFIG, - generateDynamicValidationSchema, -} from '@/schemas/dynamicValidationSchema' +import { generateDynamicValidationSchema } from '@/schemas/dynamicValidationSchema' import { createFamily, updateFamily } from '@/api/Families' import { deleteDocument } from '@/api/Documents' -import { baseFamilySchema, createFamilySchema } from '@/schemas/familySchema' +import { deleteEvent } from '@/api/Events' +import { createFamilySchema } from '@/schemas/familySchema' import { ApiError } from '../feedback/ApiError' interface FamilyFormProps { @@ -307,6 +304,34 @@ export const FamilyForm: React.FC = ({ setUpdatedEvent(eventId) } + const onEventDeleteClick = async (eventId: string) => { + toast({ + title: 'Event deletion in progress', + status: 'info', + }) + await deleteEvent(eventId) + .then(() => { + toast({ + title: 'Event has been successfully deleted', + status: 'success', + }) + const index = familyEvents.indexOf(eventId) + if (index > -1) { + const newEvents = [...familyEvents] + newEvents.splice(index, 1) + setFamilyEvents(newEvents) + } + setUpdatedEvent(eventId) + }) + .catch((error: IError) => { + toast({ + title: 'Event has not been deleted', + description: error.message, + status: 'error', + }) + }) + } + const canLoadForm = !configLoading && !collectionsLoading && !configError && !collectionsError @@ -508,10 +533,10 @@ export const FamilyForm: React.FC = ({ userCanModify={userAccess.canModify} onAddNew={onAddNewEntityClick} onEdit={onEditEntityClick} + onDelete={onEventDeleteClick} updatedEvent={updatedEvent} setUpdatedEvent={setUpdatedEvent} isNewFamily={!loadedFamily} - onSetFamilyEvents={setFamilyEvents} /> diff --git a/src/components/forms/fields/SelectField.tsx b/src/components/forms/fields/SelectField.tsx index a576746..417ef48 100644 --- a/src/components/forms/fields/SelectField.tsx +++ b/src/components/forms/fields/SelectField.tsx @@ -1,8 +1,8 @@ import React from 'react' -import { Controller, Control, FieldErrors } from 'react-hook-form' +import { Controller, Control } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' import { chakraStylesSelect } from '@/styles/chakra' -import { FieldType, SelectOption } from '@/types/metadata' +import { SelectOption } from '@/types/metadata' import { generateSelectOptions } from '@/utils/metadataUtils' import { FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react' diff --git a/src/components/forms/sections/EventSection.tsx b/src/components/forms/sections/EventSection.tsx index 83ba474..da29d4a 100644 --- a/src/components/forms/sections/EventSection.tsx +++ b/src/components/forms/sections/EventSection.tsx @@ -1,5 +1,14 @@ import React from 'react' -import { FamilyEventList } from '@/components/lists/FamilyEventList' +import { + Box, + Button, + Divider, + AbsoluteCenter, + Flex, + Text, +} from '@chakra-ui/react' +import { WarningIcon } from '@chakra-ui/icons' +import { FamilyEvent } from '@/components/family/FamilyEvent' import { IEvent } from '@/interfaces' interface EventSectionProps { @@ -7,10 +16,10 @@ interface EventSectionProps { userCanModify: boolean onAddNew: (type: 'event') => void onEdit: (type: 'event', event: IEvent) => void + onDelete: (eventId: string) => void updatedEvent: string setUpdatedEvent: (id: string) => void isNewFamily: boolean - onSetFamilyEvents: (events: string[]) => void } export const EventSection: React.FC = ({ @@ -18,21 +27,60 @@ export const EventSection: React.FC = ({ userCanModify, onAddNew, onEdit, + onDelete, updatedEvent, setUpdatedEvent, isNewFamily, - onSetFamilyEvents, }) => { return ( - onEdit('event', event)} - onAddNewEntityClick={() => onAddNew('event')} - loadedFamily={!isNewFamily} - updatedEvent={updatedEvent} - setUpdatedEvent={setUpdatedEvent} - /> + <> + + + + Events + + + + {isNewFamily && ( + + Please create the family first before attempting to add events + + )} + + {familyEvents.length > 0 && ( + + {familyEvents.map((eventId) => ( + onEdit('event', event)} + onDeleteClick={onDelete} + updatedEvent={updatedEvent} + setUpdatedEvent={setUpdatedEvent} + /> + ))} + + )} + + {!isNewFamily && ( + + + + )} + ) } diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 95c9cf8..84f32e3 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -80,10 +80,12 @@ export const MetadataSection: React.FC = ({ ({ - deleteEvent: vi.fn(), -})) // Mock the useEvent hook vi.mock('@/hooks/useEvent', () => ({ default: vi.fn().mockReturnValue({ event: { date: mockEvent.date, - event_title: 'Test event title', + event_title: 'Some event title', event_type_value: 'Appealed', }, error: null, @@ -23,28 +17,14 @@ vi.mock('@/hooks/useEvent', () => ({ }), })) -// Mock the FamilyEvent component -vi.mock('@/components/family/FamilyEvent', () => ({ - FamilyEvent: ({ eventId, onEditClick, onDeleteClick }: any) => ( -
-
Test event title
-
Type: Appealed
-
Date: {formatDate(mockEvent.date)}
- - -
- ), -})) - describe('EventSection', () => { const mockOnAddNew = vi.fn() const mockOnEdit = vi.fn() const mockOnDelete = vi.fn() const mockSetUpdatedEvent = vi.fn() - const mockSetFamilyEvents = vi.fn() const defaultProps = { - familyEvents: ['test-event-1', 'test-event-2'], + familyEvents: ['test-event-1'], userCanModify: true, onAddNew: mockOnAddNew, onEdit: mockOnEdit, @@ -56,7 +36,6 @@ describe('EventSection', () => { beforeEach(() => { vi.clearAllMocks() - ;(deleteEvent as jest.Mock).mockClear() }) it('renders existing family events', () => { @@ -65,7 +44,7 @@ describe('EventSection', () => { const formattedDate = formatDate(mockEvent.date) expect(screen.getByText('Events')).toBeInTheDocument() - expect(screen.getByText('Test event title')).toBeInTheDocument() + expect(screen.getByText('Some event title')).toBeInTheDocument() expect(screen.getByText('Type: Appealed')).toBeInTheDocument() expect(screen.getByText(`Date: ${formattedDate}`)).toBeInTheDocument() expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2) @@ -80,7 +59,11 @@ describe('EventSection', () => { it('does not render add event button for new family', () => { render() - expect(screen.getByText('Please create the family first before attempting to add events')).toBeInTheDocument() + expect( + screen.getByText( + 'Please create the family first before attempting to add events', + ), + ).toBeInTheDocument() expect(screen.queryByText('Add new Event')).not.toBeInTheDocument() }) @@ -88,67 +71,10 @@ describe('EventSection', () => { render() const addButton = screen.getByText('Add new Event') - expect(addButton).toHaveAttribute('data-test-id', 'warning-icon-event') - }) - - it('calls onAddNew when add event button is clicked', () => { - render() - - fireEvent.click(screen.getByText('Add new Event')) - expect(mockOnAddNew).toHaveBeenCalledWith('event') - }) - - it('calls onEdit when edit button is clicked', () => { - render() - - const editButtons = screen.getAllByText('Edit') - fireEvent.click(editButtons[0]) - expect(mockOnEdit).toHaveBeenCalledWith('event', mockEvent) - }) - - it('calls onDelete when delete button is clicked', async () => { - // Mock successful deletion - ;(deleteEvent as jest.Mock).mockResolvedValue({}) - - const familyEvents = ['test-event-1', 'test-event-2'] - const props = { - ...defaultProps, - familyEvents, - setFamilyEvents: mockSetFamilyEvents, - } - - render() - - const deleteButtons = screen.getAllByText('Delete') - fireEvent.click(deleteButtons[0]) - - await waitFor(() => { - expect(deleteEvent).toHaveBeenCalledWith('test-event-1') - expect(mockSetFamilyEvents).toHaveBeenCalledWith(['test-event-2']) - }) - }) - - it('handles delete error', async () => { - // Mock failed deletion - const mockError = new Error('Deletion failed') - ;(deleteEvent as jest.Mock).mockRejectedValue(mockError) - - const familyEvents = ['test-event-1', 'test-event-2'] - const props = { - ...defaultProps, - familyEvents, - setFamilyEvents: mockSetFamilyEvents, - } - - render() - - const deleteButtons = screen.getAllByText('Delete') - fireEvent.click(deleteButtons[0]) - - await waitFor(() => { - expect(deleteEvent).toHaveBeenCalledWith('test-event-1') - expect(mockSetFamilyEvents).not.toHaveBeenCalled() - }) + const warningIcon = addButton.querySelector( + 'svg[data-test-id="warning-icon-event"]', + ) + expect(warningIcon).toBeInTheDocument() }) it('disables add event button when user cannot modify', () => { From 2bbe5f6bb4a1801301692fbd80c0a0dfafb2b735 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:57:43 +0000 Subject: [PATCH 28/88] Rename metadata.ts to Metadata.ts --- src/components/forms/DynamicMetadataFields.tsx | 2 +- src/{types/metadata.ts => interfaces/Metadata.ts} | 0 src/schemas/dynamicValidationSchema.ts | 2 +- src/utils/metadataUtils.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/{types/metadata.ts => interfaces/Metadata.ts} (100%) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index b3a49d5..78eaf26 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -6,7 +6,7 @@ import { FormHelperText, } from '@chakra-ui/react' import { Control, FieldErrors } from 'react-hook-form' -import { FieldType } from '@/types/metadata' +import { FieldType } from '@/interfaces/Metadata' import { formatFieldLabel } from '@/utils/metadataUtils' import { SelectField } from './fields/SelectField' import { TextField } from './fields/TextField' diff --git a/src/types/metadata.ts b/src/interfaces/Metadata.ts similarity index 100% rename from src/types/metadata.ts rename to src/interfaces/Metadata.ts diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index a014d60..2e7f116 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -4,7 +4,7 @@ import { Taxonomy, CorpusInfo, CORPUS_METADATA_CONFIG, -} from '@/types/metadata' +} from '@/interfaces/Metadata' export const generateDynamicValidationSchema = ( taxonomy: Taxonomy | null, diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index 6b7368a..e9edb2f 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -1,4 +1,4 @@ -import { SelectOption } from '@/types/metadata' +import { SelectOption } from '@/interfaces/Metadata' export const formatFieldLabel = (key: string): string => { return key From 5974ba7772fc239aa423fddfdd7feca1ed57e273 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:57:51 +0000 Subject: [PATCH 29/88] Rename metadata.ts to Metadata.ts --- src/components/forms/fields/SelectField.tsx | 2 +- src/components/forms/sections/MetadataSection.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/forms/fields/SelectField.tsx b/src/components/forms/fields/SelectField.tsx index 417ef48..9f4f869 100644 --- a/src/components/forms/fields/SelectField.tsx +++ b/src/components/forms/fields/SelectField.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Controller, Control } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' import { chakraStylesSelect } from '@/styles/chakra' -import { SelectOption } from '@/types/metadata' +import { SelectOption } from '@/interfaces/Metadata' import { generateSelectOptions } from '@/utils/metadataUtils' import { FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react' diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 84f32e3..e42c721 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react' import { Control, FieldErrors, UseFormReset } from 'react-hook-form' import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' import { DynamicMetadataFields } from '../DynamicMetadataFields' -import { CORPUS_METADATA_CONFIG, FieldType } from '@/types/metadata' +import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' import { IConfigCorpus, TFamily } from '@/interfaces' interface MetadataSectionProps { From 0410e0c061a5e9e7291f20f10f3f5dca6945d0f0 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:20:12 +0000 Subject: [PATCH 30/88] Fix some eslint erros --- src/components/drawers/DocumentEditDrawer.tsx | 6 +- src/components/drawers/EntityEditDrawer.tsx | 12 +- src/components/drawers/EventEditDrawer.tsx | 6 +- src/components/forms/EventForm.tsx | 128 +++++++++--------- src/components/forms/FamilyForm.tsx | 108 +++++++++------ src/components/forms/fields/SelectField.tsx | 4 +- src/components/forms/fields/WYSIWYGField.tsx | 2 +- .../forms/sections/MetadataSection.tsx | 60 ++++---- src/hooks/useTaxonomy.ts | 19 --- src/interfaces/Config.ts | 11 +- src/interfaces/Metadata.ts | 26 +--- src/schemas/dynamicValidationSchema.ts | 8 +- src/views/document/Document.tsx | 3 +- 13 files changed, 193 insertions(+), 200 deletions(-) delete mode 100644 src/hooks/useTaxonomy.ts diff --git a/src/components/drawers/DocumentEditDrawer.tsx b/src/components/drawers/DocumentEditDrawer.tsx index c048b3e..f11206a 100644 --- a/src/components/drawers/DocumentEditDrawer.tsx +++ b/src/components/drawers/DocumentEditDrawer.tsx @@ -6,17 +6,17 @@ import { DrawerHeader, DrawerOverlay, } from '@chakra-ui/react' -import { TDocument } from '@/interfaces' +import { IDocument, TTaxonomy } from '@/interfaces' import { DocumentForm } from '../forms/DocumentForm' interface DocumentEditDrawerProps { - document?: TDocument + document?: IDocument familyId?: string onClose: () => void isOpen: boolean onSuccess?: (documentId: string) => void canModify?: boolean - taxonomy?: any + taxonomy?: TTaxonomy } export const DocumentEditDrawer: React.FC = ({ diff --git a/src/components/drawers/EntityEditDrawer.tsx b/src/components/drawers/EntityEditDrawer.tsx index b231f9c..86af593 100644 --- a/src/components/drawers/EntityEditDrawer.tsx +++ b/src/components/drawers/EntityEditDrawer.tsx @@ -1,18 +1,18 @@ import React from 'react' import { DocumentEditDrawer } from './DocumentEditDrawer' import { EventEditDrawer } from './EventEditDrawer' -import { TDocument, TEvent } from '@/interfaces' +import { IDocument, IEvent, TTaxonomy } from '@/interfaces' interface EntityEditDrawerProps { isOpen: boolean onClose: () => void entity: 'document' | 'event' - document?: TDocument - event?: TEvent - onDocumentSuccess?: (document: TDocument) => void - onEventSuccess?: (event: TEvent) => void + document?: IDocument + event?: IEvent + onDocumentSuccess?: (document: IDocument) => void + onEventSuccess?: (event: IEvent) => void familyId?: string - taxonomy?: any + taxonomy?: TTaxonomy canModify?: boolean } diff --git a/src/components/drawers/EventEditDrawer.tsx b/src/components/drawers/EventEditDrawer.tsx index 4f84647..4873e21 100644 --- a/src/components/drawers/EventEditDrawer.tsx +++ b/src/components/drawers/EventEditDrawer.tsx @@ -6,18 +6,18 @@ import { DrawerHeader, DrawerOverlay, } from '@chakra-ui/react' -import { TEvent } from '@/interfaces' +import { IEvent, TTaxonomy } from '@/interfaces' import { EventForm } from '../forms/EventForm' import { formatDate } from '@/utils/formatDate' interface EventEditDrawerProps { - event?: TEvent + event?: IEvent familyId?: string onClose: () => void isOpen: boolean onSuccess?: (eventId: string) => void canModify?: boolean - taxonomy?: any + taxonomy?: TTaxonomy } export const EventEditDrawer: React.FC = ({ diff --git a/src/components/forms/EventForm.tsx b/src/components/forms/EventForm.tsx index 7b69e71..39d0ee9 100644 --- a/src/components/forms/EventForm.tsx +++ b/src/components/forms/EventForm.tsx @@ -24,6 +24,7 @@ import { Select, } from '@chakra-ui/react' import { formatDateISO } from '@/utils/formatDate' +import { ApiError } from '../feedback/ApiError' type TaxonomyEventType = | { event_type: string[] } @@ -153,67 +154,70 @@ export const EventForm = ({ const invalidEventCreation = !loadedEvent && !familyId return ( -
- - - Event Title - - {errors.event_title && ( - {errors.event_title.message} - )} - - - - Date - - {errors.date && ( - {errors.date.message} - )} - - - - Event Type - - {errors.event_type_value && ( - - {errors.event_type_value.message} - - )} - - - - - - -
+ <> +
+ + {formError && } + + Event Title + + {errors.event_title && ( + {errors.event_title.message} + )} + + + + Date + + {errors.date && ( + {errors.date.message} + )} + + + + Event Type + + {errors.event_type_value && ( + + {errors.event_type_value.message} + + )} + + + + + + +
+ ) } diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 5d2c04a..f1079ce 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useMemo } from 'react' +import { useEffect, useState, useMemo, useCallback } from 'react' import { useForm, SubmitHandler } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { useBlocker, useNavigate } from 'react-router-dom' @@ -14,7 +14,6 @@ import { import useCorpusFromConfig from '@/hooks/useCorpusFromConfig' import useConfig from '@/hooks/useConfig' -import useTaxonomy from '@/hooks/useTaxonomy' import useCollections from '@/hooks/useCollections' import { SelectField } from './fields/SelectField' @@ -33,8 +32,8 @@ import { IInternationalAgreementsMetadata, ILawsAndPoliciesMetadata, TFamilyFormPostMetadata, + TFamily, } from '@/interfaces/Family' -import { TChildEntity } from '@/types/entities' import { canModify } from '@/utils/canModify' import { getCountries } from '@/utils/extractNestedGeographyData' import { decodeToken } from '@/utils/decodeToken' @@ -45,11 +44,27 @@ import { deleteDocument } from '@/api/Documents' import { deleteEvent } from '@/api/Events' import { createFamilySchema } from '@/schemas/familySchema' import { ApiError } from '../feedback/ApiError' +// import { IChakraSelect } from '@/interfaces/Config' +import { IDocument } from '@/interfaces/Document' +import { IEvent } from '@/interfaces/Event' +import { IError } from '@/interfaces/Auth' +import { IConfigCorpora, TTaxonomy } from '@/interfaces' interface FamilyFormProps { family?: TFamily } +type TChildEntity = 'event' | 'document' + +// interface IFamilyFormBase { +// title: string +// summary: string +// geography: IChakraSelect +// category: string +// corpus: IChakraSelect +// collections?: IChakraSelect[] +// } + export const FamilyForm: React.FC = ({ family: loadedFamily, }) => { @@ -66,38 +81,48 @@ export const FamilyForm: React.FC = ({ const toast = useToast() const [formError, setFormError] = useState() + // Determine corpus import ID based on loaded family or form input + const getCorpusImportId = ( + loadedFamily?: TFamily, + watchCorpus?: { value: string }, + ) => loadedFamily?.corpus_import_id || watchCorpus?.value + // Initialise corpus and taxonomy first const initialCorpusInfo = useCorpusFromConfig( config?.corpora, - loadedFamily?.corpus_import_id, - loadedFamily?.corpus_import_id, + getCorpusImportId(loadedFamily), + getCorpusImportId(loadedFamily), ) - const initialTaxonomy = useTaxonomy( - initialCorpusInfo?.corpus_type, - initialCorpusInfo?.taxonomy, - loadedFamily?.corpus_import_id, + const initialTaxonomy = initialCorpusInfo?.taxonomy + + // Create validation schema + const createValidationSchema = useCallback( + (currentTaxonomy?: TTaxonomy, currentCorpusInfo?: IConfigCorpora) => { + const metadataSchema = + generateDynamicValidationSchema( + currentTaxonomy, + currentCorpusInfo, + ) + return createFamilySchema(metadataSchema) + }, + [], ) - // Create initial validation schema - const validationSchema = useMemo(() => { - const metadataSchema = generateDynamicValidationSchema( - initialTaxonomy, - initialCorpusInfo, - ) - return createFamilySchema(metadataSchema) - }, [initialTaxonomy, initialCorpusInfo]) + // Initial validation schema + const validationSchema = useMemo( + () => createValidationSchema(initialTaxonomy, initialCorpusInfo), + [initialTaxonomy, initialCorpusInfo, createValidationSchema], + ) const { control, - register, handleSubmit, reset, - setError, setValue, watch, formState: { errors, isSubmitting, dirtyFields }, trigger, - } = useForm({ + } = useForm({ resolver: yupResolver(validationSchema), }) @@ -105,22 +130,17 @@ export const FamilyForm: React.FC = ({ const watchCorpus = watch('corpus') const corpusInfo = useCorpusFromConfig( config?.corpora, - loadedFamily?.corpus_import_id, - watchCorpus?.value, - ) - const taxonomy = useTaxonomy( - corpusInfo?.corpus_type, - corpusInfo?.taxonomy, - watchCorpus?.value, + getCorpusImportId(loadedFamily), + getCorpusImportId(loadedFamily, watchCorpus), ) + const taxonomy = corpusInfo?.taxonomy // Update validation schema when corpus/taxonomy changes useEffect(() => { - const metadataSchema = generateDynamicValidationSchema(taxonomy, corpusInfo) - const newSchema = createFamilySchema(metadataSchema) + const newSchema = createValidationSchema(taxonomy, corpusInfo) // Re-trigger form validation with new schema trigger() - }, [taxonomy, corpusInfo]) + }, [taxonomy, corpusInfo, createValidationSchema, trigger]) // Determine if the corpus is an MCF type const isMCFCorpus = useMemo(() => { @@ -149,14 +169,14 @@ export const FamilyForm: React.FC = ({ return { canModify: canModify( loadedFamily ? String(loadedFamily.organisation) : null, - decodedToken?.is_superuser, + decodedToken?.is_superuser ?? false, decodedToken?.authorisation, ), isSuperUser: decodedToken?.is_superuser || false, } }, [loadedFamily]) - const handleFormSubmission = async (formData: IFamilyFormPost) => { + const handleFormSubmission = async (formData: TFamilyFormPost) => { setIsFormSubmitting(true) setFormError(null) @@ -175,14 +195,18 @@ export const FamilyForm: React.FC = ({ // Handle Laws and Policies metadata else if (corpusInfo?.corpus_type === 'Laws and Policies') { const lawsPoliciesMetadata: ILawsAndPoliciesMetadata = { - topic: formData.topic?.map((topic) => topic.value) || [], - hazard: formData.hazard?.map((hazard) => hazard.value) || [], - sector: formData.sector?.map((sector) => sector.value) || [], - keyword: formData.keyword?.map((keyword) => keyword.value) || [], + topic: formData.topic?.map((topic) => topic.value as string) || [], + hazard: formData.hazard?.map((hazard) => hazard.value as string) || [], + sector: formData.sector?.map((sector) => sector.value as string) || [], + keyword: + formData.keyword?.map((keyword) => keyword.value as string) || [], framework: - formData.framework?.map((framework) => framework.value) || [], + formData.framework?.map((framework) => framework.value as string) || + [], instrument: - formData.instrument?.map((instrument) => instrument.value) || [], + formData.instrument?.map( + (instrument) => instrument.value as string, + ) || [], } Object.assign(familyMetadata, lawsPoliciesMetadata) } @@ -226,7 +250,7 @@ export const FamilyForm: React.FC = ({ } } - const onSubmit: SubmitHandler = async (data) => { + const onSubmit: SubmitHandler = async (data) => { try { await handleFormSubmission(data) } catch (error) { @@ -340,7 +364,7 @@ export const FamilyForm: React.FC = ({ setFamilyDocuments(loadedFamily.documents || []) setFamilyEvents(loadedFamily.events || []) - const resetValues: Partial = { + const resetValues: Partial = { title: loadedFamily.title, summary: loadedFamily.summary, collections: @@ -415,8 +439,8 @@ export const FamilyForm: React.FC = ({ detail='Please go back to the "Families" page, if you think there has been a mistake please contact the administrator.' /> )} - {(configError || collectionsError) && ( - + {(configError || collectionsError || formError) && ( + )} {canLoadForm && ( diff --git a/src/components/forms/fields/SelectField.tsx b/src/components/forms/fields/SelectField.tsx index 9f4f869..da7a2a2 100644 --- a/src/components/forms/fields/SelectField.tsx +++ b/src/components/forms/fields/SelectField.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Controller, Control } from 'react-hook-form' +import { Controller, Control, RegisterOptions } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' import { chakraStylesSelect } from '@/styles/chakra' import { SelectOption } from '@/interfaces/Metadata' @@ -12,7 +12,7 @@ interface SelectFieldProps> { control: Control options: string[] | SelectOption[] isMulti?: boolean - rules?: any + rules?: RegisterOptions isRequired?: boolean } diff --git a/src/components/forms/fields/WYSIWYGField.tsx b/src/components/forms/fields/WYSIWYGField.tsx index ae4051c..296273d 100644 --- a/src/components/forms/fields/WYSIWYGField.tsx +++ b/src/components/forms/fields/WYSIWYGField.tsx @@ -28,7 +28,7 @@ export const WYSIWYGField = ({ name={name} rules={{ required: isRequired ? 'This field is required' : false, - validate: (value) => + validate: (value: string) => isRequired ? (value && value.trim() !== '') || 'This field is required' : true, diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index e42c721..4e84e68 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -3,11 +3,11 @@ import { Control, FieldErrors, UseFormReset } from 'react-hook-form' import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' import { DynamicMetadataFields } from '../DynamicMetadataFields' import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' -import { IConfigCorpus, TFamily } from '@/interfaces' +import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' interface MetadataSectionProps { - corpusInfo: IConfigCorpus - taxonomy: any + corpusInfo: IConfigCorpora + taxonomy: TTaxonomy control: Control errors: FieldErrors loadedFamily?: TFamily @@ -24,37 +24,31 @@ export const MetadataSection: React.FC = ({ }) => { useEffect(() => { if (loadedFamily?.metadata && corpusInfo) { - const metadataValues = Object.entries(loadedFamily.metadata).reduce( - (acc, [key, value]) => { - const fieldConfig = - CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] - if (!fieldConfig) return acc + const metadataValues = Object.entries(loadedFamily.metadata).reduce< + Record + >((acc, [key, value]) => { + const fieldConfig = + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] + if (!fieldConfig) return acc - if (fieldConfig.type === FieldType.SINGLE_SELECT) { - acc[key] = value?.[0] - ? { - value: value[0], - label: value[0], - } - : undefined - } else if (fieldConfig.type === FieldType.MULTI_SELECT) { - acc[key] = - value?.map((v) => ({ - value: v, - label: v, - })) || [] - } else if ( - fieldConfig.type === FieldType.TEXT || - fieldConfig.type === FieldType.NUMBER - ) { - acc[key] = value?.[0] || '' - } else { - acc[key] = value?.[0] || '' - } - return acc - }, - {} as Record, - ) + if (fieldConfig.type === FieldType.SINGLE_SELECT) { + acc[key] = value?.[0] + ? { + value: value[0], + label: value[0], + } + : undefined + } else if (fieldConfig.type === FieldType.MULTI_SELECT) { + acc[key] = value?.map((v) => ({ + value: v, + label: v, + })) + } else { + acc[key] = value?.[0] + } + + return acc + }, {}) reset((formValues) => ({ ...formValues, diff --git a/src/hooks/useTaxonomy.ts b/src/hooks/useTaxonomy.ts deleted file mode 100644 index 3714058..0000000 --- a/src/hooks/useTaxonomy.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IConfigTaxonomyCCLW, IConfigTaxonomyUNFCCC } from '@/interfaces/Config' -import { useMemo } from 'react' - -// TODO: Not sure whether we need to maintain this? -const useTaxonomy = ( - corpus_type?: string, - corpus_taxonomy?: IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC, -) => { - return useMemo(() => { - if (corpus_type === 'Law and Policies') - return corpus_taxonomy as IConfigTaxonomyCCLW - else if (corpus_type === 'Intl. agreements') - return corpus_taxonomy as IConfigTaxonomyUNFCCC - else if (corpus_type === 'AF') return corpus_taxonomy - else return corpus_taxonomy - }, [corpus_type, corpus_taxonomy]) -} - -export default useTaxonomy diff --git a/src/interfaces/Config.ts b/src/interfaces/Config.ts index 0a6ca3d..648a116 100644 --- a/src/interfaces/Config.ts +++ b/src/interfaces/Config.ts @@ -13,7 +13,7 @@ export interface IConfigGeography { node: IConfigGeographyNode children: IConfigGeography[] } -interface IChakraSelect extends OptionBase { +export interface IChakraSelect extends OptionBase { value: string label: string } @@ -42,6 +42,7 @@ export interface IConfigTaxonomyCCLW { instrument: IConfigMeta event_type: IConfigMeta _document: IConfigDocumentMetadata + _event: IConfigEventMetadata } export interface IConfigTaxonomyUNFCCC { @@ -49,8 +50,14 @@ export interface IConfigTaxonomyUNFCCC { author_type: IConfigMeta event_type: IConfigMeta _document: IConfigDocumentMetadata + _event: IConfigEventMetadata } +export type TTaxonomy = IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC + +export interface IConfigEventMetadata { + event_type: IConfigMeta +} export interface IConfigDocumentMetadata { role: IConfigMeta type: IConfigMeta @@ -67,7 +74,7 @@ export interface IConfigCorpora { corpus_type: string corpus_type_description: string organisation: IConfigOrganisationMetadata - taxonomy: IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC + taxonomy: TTaxonomy } export interface IConfig { diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index fc55c3f..6bb0051 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -19,6 +19,10 @@ export interface Taxonomy { [key: string]: TaxonomyField } +export interface SubTaxonomy { + [key: string]: Taxonomy +} + export interface CorpusInfo { corpus_type: string title?: string // TODO Do we need this? @@ -78,28 +82,6 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { 'instrument', ], }, - AF: { - renderFields: { - region: { type: FieldType.MULTI_SELECT }, - sector: { type: FieldType.MULTI_SELECT }, - implementing_agency: { type: FieldType.MULTI_SELECT }, - status: { type: FieldType.SINGLE_SELECT }, - project_id: { type: FieldType.TEXT }, - project_url: { type: FieldType.TEXT }, - project_value_co_financing: { type: FieldType.NUMBER }, - project_value_fund_spend: { type: FieldType.NUMBER }, - }, - validationFields: [ - 'project_id', - 'project_url', - 'region', - 'sector', - 'status', - 'implementing_agency', - 'project_value_co_financing', - 'project_value_fund_spend', - ], - }, default: { renderFields: {}, validationFields: [], diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 2e7f116..f703ea0 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -6,10 +6,12 @@ import { CORPUS_METADATA_CONFIG, } from '@/interfaces/Metadata' -export const generateDynamicValidationSchema = ( +export const generateDynamicValidationSchema = < + T extends Record, +>( taxonomy: Taxonomy | null, corpusInfo: CorpusInfo | null, -): yup.ObjectSchema => { +): yup.ObjectSchema => { if (!taxonomy || !corpusInfo) { return yup.object({}).required() } @@ -85,7 +87,7 @@ export const generateDynamicValidationSchema = ( [fieldKey]: fieldValidation, } }, - {}, + {} as T, ) return yup.object(schemaShape).required() diff --git a/src/views/document/Document.tsx b/src/views/document/Document.tsx index dfa584a..774d71a 100644 --- a/src/views/document/Document.tsx +++ b/src/views/document/Document.tsx @@ -15,7 +15,6 @@ import { ApiError } from '@/components/feedback/ApiError' import useDocument from '@/hooks/useDocument' import useFamily from '@/hooks/useFamily' import useConfig from '@/hooks/useConfig' -import useTaxonomy from '@/hooks/useTaxonomy' import { decodeToken } from '@/utils/decodeToken' import { IDecodedToken } from '@/interfaces' import { canModify } from '@/utils/canModify' @@ -34,7 +33,7 @@ export default function Document() { config?.corpora, family?.corpus_import_id, ) - const taxonomy = useTaxonomy(corpusInfo?.corpus_type, corpusInfo?.taxonomy) + const taxonomy = corpusInfo?.taxonomy const userToken = useMemo(() => { const token = localStorage.getItem('token') if (!token) return null From 3fd1f01ed819236409698267ef908a75baa35910 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:54:56 +0000 Subject: [PATCH 31/88] Allow conditional render of metadata --- src/components/forms/FamilyForm.tsx | 10 ++++------ src/components/forms/sections/MetadataSection.tsx | 6 +++--- src/schemas/dynamicValidationSchema.ts | 3 ++- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index f1079ce..62197b9 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -98,11 +98,10 @@ export const FamilyForm: React.FC = ({ // Create validation schema const createValidationSchema = useCallback( (currentTaxonomy?: TTaxonomy, currentCorpusInfo?: IConfigCorpora) => { - const metadataSchema = - generateDynamicValidationSchema( - currentTaxonomy, - currentCorpusInfo, - ) + const metadataSchema = generateDynamicValidationSchema( + currentTaxonomy, + currentCorpusInfo, + ) return createFamilySchema(metadataSchema) }, [], @@ -537,7 +536,6 @@ export const FamilyForm: React.FC = ({ loadedFamily={loadedFamily} reset={reset} /> - )} diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 4e84e68..9ef70ec 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -6,8 +6,8 @@ import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' interface MetadataSectionProps { - corpusInfo: IConfigCorpora - taxonomy: TTaxonomy + corpusInfo?: IConfigCorpora + taxonomy?: TTaxonomy control: Control errors: FieldErrors loadedFamily?: TFamily @@ -44,7 +44,7 @@ export const MetadataSection: React.FC = ({ label: v, })) } else { - acc[key] = value?.[0] + acc[key] = value } return acc diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index f703ea0..6acce12 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -5,9 +5,10 @@ import { CorpusInfo, CORPUS_METADATA_CONFIG, } from '@/interfaces/Metadata' +import { TTaxonomy } from '@/interfaces' export const generateDynamicValidationSchema = < - T extends Record, + T extends TTaxonomy, >( taxonomy: Taxonomy | null, corpusInfo: CorpusInfo | null, From b03ec0ab0f9656799eb7783a5bf49deeb2c5dae3 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:32:16 +0000 Subject: [PATCH 32/88] Remove unused imports --- src/components/forms/FamilyForm.tsx | 1 - src/schemas/dynamicValidationSchema.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 62197b9..3fc2f94 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -8,7 +8,6 @@ import { ButtonGroup, useToast, SkeletonText, - Divider, useDisclosure, } from '@chakra-ui/react' diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 6acce12..27c8253 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -7,9 +7,7 @@ import { } from '@/interfaces/Metadata' import { TTaxonomy } from '@/interfaces' -export const generateDynamicValidationSchema = < - T extends TTaxonomy, ->( +export const generateDynamicValidationSchema = ( taxonomy: Taxonomy | null, corpusInfo: CorpusInfo | null, ): yup.ObjectSchema => { From 117fc44f6b1693fadfb5f1ca69e7d112cc2d0307 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:46:32 +0000 Subject: [PATCH 33/88] Type errors --- src/components/forms/DocumentForm.tsx | 21 +++--- src/components/forms/FamilyForm.tsx | 13 +--- .../forms/sections/MetadataSection.tsx | 10 +-- src/interfaces/Config.ts | 68 +++++++++++-------- src/interfaces/Metadata.ts | 26 ++----- src/schemas/dynamicValidationSchema.ts | 7 +- src/schemas/familySchema.ts | 28 +++++++- 7 files changed, 96 insertions(+), 77 deletions(-) diff --git a/src/components/forms/DocumentForm.tsx b/src/components/forms/DocumentForm.tsx index d0b70b6..64520a1 100644 --- a/src/components/forms/DocumentForm.tsx +++ b/src/components/forms/DocumentForm.tsx @@ -11,8 +11,7 @@ import { IDocumentFormPostModified, IDocumentMetadata, IError, - IConfigTaxonomyCCLW, - IConfigTaxonomyUNFCCC, + TTaxonomy, } from '@/interfaces' import { createDocument, updateDocument } from '@/api/Documents' import { documentSchema } from '@/schemas/documentSchema' @@ -41,7 +40,7 @@ type TProps = { document?: IDocument familyId?: string canModify?: boolean - taxonomy?: IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC + taxonomy?: TTaxonomy onSuccess?: (documentId: string) => void } @@ -70,7 +69,9 @@ export const DocumentForm = ({ // Ensure family_import_id is always set useEffect(() => { if (familyId || loadedDocument?.family_import_id) { - setValue('family_import_id', familyId || loadedDocument?.family_import_id) + if (familyId) setValue('family_import_id', familyId) + else if (loadedDocument) + setValue('family_import_id', loadedDocument?.family_import_id) } }, [familyId, loadedDocument, setValue]) @@ -229,11 +230,13 @@ export const DocumentForm = ({ Role Please select a role
diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 3fc2f94..abf46c2 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -55,15 +55,6 @@ interface FamilyFormProps { type TChildEntity = 'event' | 'document' -// interface IFamilyFormBase { -// title: string -// summary: string -// geography: IChakraSelect -// category: string -// corpus: IChakraSelect -// collections?: IChakraSelect[] -// } - export const FamilyForm: React.FC = ({ family: loadedFamily, }) => { @@ -92,7 +83,7 @@ export const FamilyForm: React.FC = ({ getCorpusImportId(loadedFamily), getCorpusImportId(loadedFamily), ) - const initialTaxonomy = initialCorpusInfo?.taxonomy + const initialTaxonomy = initialCorpusInfo ? initialCorpusInfo?.taxonomy : null // Create validation schema const createValidationSchema = useCallback( @@ -135,7 +126,7 @@ export const FamilyForm: React.FC = ({ // Update validation schema when corpus/taxonomy changes useEffect(() => { - const newSchema = createValidationSchema(taxonomy, corpusInfo) + createValidationSchema(taxonomy, corpusInfo) // Re-trigger form validation with new schema trigger() }, [taxonomy, corpusInfo, createValidationSchema, trigger]) diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 9ef70ec..db3d95c 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -5,16 +5,16 @@ import { DynamicMetadataFields } from '../DynamicMetadataFields' import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' -interface MetadataSectionProps { +interface MetadataSectionProps> { corpusInfo?: IConfigCorpora taxonomy?: TTaxonomy - control: Control - errors: FieldErrors + control: Control + errors: FieldErrors loadedFamily?: TFamily - reset: UseFormReset + reset: UseFormReset } -export const MetadataSection: React.FC = ({ +export const MetadataSection: React.FC> = ({ corpusInfo, taxonomy, control, diff --git a/src/interfaces/Config.ts b/src/interfaces/Config.ts index 648a116..92fcb63 100644 --- a/src/interfaces/Config.ts +++ b/src/interfaces/Config.ts @@ -13,6 +13,7 @@ export interface IConfigGeography { node: IConfigGeographyNode children: IConfigGeography[] } + export interface IChakraSelect extends OptionBase { value: string label: string @@ -27,42 +28,55 @@ export interface IConfigLanguageSorted extends OptionBase { label: string } -interface IConfigMeta { +// Types for taxonomy and corpus info +export interface ITaxonomyField { + allowed_values?: string[] allow_any?: boolean - allow_blanks: boolean - allowed_values: string[] + allow_blanks?: boolean } -export interface IConfigTaxonomyCCLW { - topic: IConfigMeta - hazard: IConfigMeta - sector: IConfigMeta - keyword: IConfigMeta - framework: IConfigMeta - instrument: IConfigMeta - event_type: IConfigMeta - _document: IConfigDocumentMetadata - _event: IConfigEventMetadata +interface ISubTaxonomy { + [key: string]: ITaxonomyField } -export interface IConfigTaxonomyUNFCCC { - author: IConfigMeta - author_type: IConfigMeta - event_type: IConfigMeta - _document: IConfigDocumentMetadata - _event: IConfigEventMetadata +export interface IDocumentSubTaxonomy extends ISubTaxonomy { + role: ITaxonomyField + type: ITaxonomyField } -export type TTaxonomy = IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC +export interface IEventSubTaxonomy extends ISubTaxonomy { + event_type: ITaxonomyField +} + +export type TSubTaxonomy = IEventSubTaxonomy | IDocumentSubTaxonomy + +export interface ITaxonomy { + [key: string]: ITaxonomyField | TSubTaxonomy +} -export interface IConfigEventMetadata { - event_type: IConfigMeta +export interface IConfigTaxonomyCCLW extends ITaxonomy { + topic: ITaxonomyField + hazard: ITaxonomyField + sector: ITaxonomyField + keyword: ITaxonomyField + framework: ITaxonomyField + instrument: ITaxonomyField + event_type: ITaxonomyField + _document: IDocumentSubTaxonomy + _event: IEventSubTaxonomy } -export interface IConfigDocumentMetadata { - role: IConfigMeta - type: IConfigMeta + +export interface IConfigTaxonomyUNFCCC extends ITaxonomy { + author: ITaxonomyField + author_type: ITaxonomyField + event_type: ITaxonomyField + _document: IDocumentSubTaxonomy + _event: IEventSubTaxonomy } -export interface IConfigOrganisationMetadata { + +export type TTaxonomy = IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC + +export interface IConfigOrganisationInfo { name: string id: number } @@ -73,7 +87,7 @@ export interface IConfigCorpora { description: string corpus_type: string corpus_type_description: string - organisation: IConfigOrganisationMetadata + organisation: IConfigOrganisationInfo taxonomy: TTaxonomy } diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index 6bb0051..e2d29ae 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -1,4 +1,5 @@ import { Control, FieldErrors } from 'react-hook-form' +import { ITaxonomyField } from './Config' export enum FieldType { TEXT = 'text', @@ -8,19 +9,10 @@ export enum FieldType { DATE = 'date', } -// Types for taxonomy and corpus info -export interface TaxonomyField { - allowed_values?: string[] - allow_any?: boolean - allow_blanks?: boolean -} - -export interface Taxonomy { - [key: string]: TaxonomyField -} - -export interface SubTaxonomy { - [key: string]: Taxonomy +export interface MetadataFieldConfig { + type: FieldType + label?: string + allowedValues?: string[] } export interface CorpusInfo { @@ -28,12 +20,6 @@ export interface CorpusInfo { title?: string // TODO Do we need this? } -export interface MetadataFieldConfig { - type: FieldType - label?: string - allowedValues?: string[] -} - // Enhanced configuration type for corpus metadata export interface CorpusMetadataConfig { [corpusType: string]: { @@ -49,7 +35,7 @@ export interface SelectOption { export interface DynamicMetadataFieldProps> { fieldKey: string - taxonomyField: TaxonomyField + taxonomyField: ITaxonomyField control: Control errors: FieldErrors fieldType: FieldType diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 27c8253..f6faa35 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -1,18 +1,17 @@ import * as yup from 'yup' import { FieldType, - Taxonomy, CorpusInfo, CORPUS_METADATA_CONFIG, } from '@/interfaces/Metadata' import { TTaxonomy } from '@/interfaces' export const generateDynamicValidationSchema = ( - taxonomy: Taxonomy | null, - corpusInfo: CorpusInfo | null, + taxonomy?: TTaxonomy, + corpusInfo?: CorpusInfo, ): yup.ObjectSchema => { if (!taxonomy || !corpusInfo) { - return yup.object({}).required() + return yup.object({}).required() as yup.ObjectSchema } const metadataFields = diff --git a/src/schemas/familySchema.ts b/src/schemas/familySchema.ts index 353caba..6b6e4da 100644 --- a/src/schemas/familySchema.ts +++ b/src/schemas/familySchema.ts @@ -1,5 +1,29 @@ +import { IChakraSelect } from '@/interfaces' import * as yup from 'yup' +// interface IFamilyFormBase { +// title: string +// summary: string +// geography: IChakraSelect +// category: string +// corpus: IChakraSelect +// collections?: IChakraSelect[] +// } + +interface IFamilyFormMetadata { + // Intl. agreements + author?: string + author_type?: string + + // Laws and Policies + topic?: IChakraSelect[] + hazard?: IChakraSelect[] + sector?: IChakraSelect[] + keyword?: IChakraSelect[] + framework?: IChakraSelect[] + instrument?: IChakraSelect[] +} + // Base schema for core family fields (non-metadata) export const baseFamilySchema = yup .object({ @@ -21,6 +45,8 @@ export const baseFamilySchema = yup .required() // Function to merge base schema with dynamic metadata schema -export const createFamilySchema = (metadataSchema: yup.ObjectSchema) => { +export const createFamilySchema = ( + metadataSchema: yup.ObjectSchema, +) => { return baseFamilySchema.concat(metadataSchema) } From f0a4fb1d2b1e5a513af54d4d5f89163282819680 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:58:22 +0000 Subject: [PATCH 34/88] Type errors --- src/components/forms/FamilyForm.tsx | 19 ++++++++++++++----- src/schemas/familySchema.ts | 9 --------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index abf46c2..b9c30b5 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -47,12 +47,21 @@ import { ApiError } from '../feedback/ApiError' import { IDocument } from '@/interfaces/Document' import { IEvent } from '@/interfaces/Event' import { IError } from '@/interfaces/Auth' -import { IConfigCorpora, TTaxonomy } from '@/interfaces' +import { IChakraSelect, IConfigCorpora, TTaxonomy } from '@/interfaces' interface FamilyFormProps { family?: TFamily } +interface IFamilyFormBase { + title: string + summary: string + geography: IChakraSelect + category: string + corpus: IChakraSelect + collections?: IChakraSelect[] +} + type TChildEntity = 'event' | 'document' export const FamilyForm: React.FC = ({ @@ -111,12 +120,12 @@ export const FamilyForm: React.FC = ({ watch, formState: { errors, isSubmitting, dirtyFields }, trigger, - } = useForm({ + } = useForm({ resolver: yupResolver(validationSchema), }) - // Watch for corpus changes and update schema - const watchCorpus = watch('corpus') + // Watch for corpus changes and update schema only when creating a new family + const watchCorpus = !loadedFamily ? watch('corpus') : undefined const corpusInfo = useCorpusFromConfig( config?.corpora, getCorpusImportId(loadedFamily), @@ -165,7 +174,7 @@ export const FamilyForm: React.FC = ({ } }, [loadedFamily]) - const handleFormSubmission = async (formData: TFamilyFormPost) => { + const handleFormSubmission = async (formData: IFamilyFormBase) => { setIsFormSubmitting(true) setFormError(null) diff --git a/src/schemas/familySchema.ts b/src/schemas/familySchema.ts index 6b6e4da..20a5622 100644 --- a/src/schemas/familySchema.ts +++ b/src/schemas/familySchema.ts @@ -1,15 +1,6 @@ import { IChakraSelect } from '@/interfaces' import * as yup from 'yup' -// interface IFamilyFormBase { -// title: string -// summary: string -// geography: IChakraSelect -// category: string -// corpus: IChakraSelect -// collections?: IChakraSelect[] -// } - interface IFamilyFormMetadata { // Intl. agreements author?: string From b224cd0d35a32baf02806a64267a2eab29c8807d Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:51:57 +0000 Subject: [PATCH 35/88] Make family form more type safe --- src/components/forms/FamilyForm.tsx | 124 +++++++++++++++++++++------- src/interfaces/Family.ts | 6 +- src/schemas/familySchema.ts | 19 +---- 3 files changed, 98 insertions(+), 51 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index b9c30b5..f6df919 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -32,6 +32,9 @@ import { ILawsAndPoliciesMetadata, TFamilyFormPostMetadata, TFamily, + IFamilyFormPostBase, + IInternationalAgreementsFamilyFormPost, + ILawsAndPoliciesFamilyFormPost, } from '@/interfaces/Family' import { canModify } from '@/utils/canModify' import { getCountries } from '@/utils/extractNestedGeographyData' @@ -62,6 +65,24 @@ interface IFamilyFormBase { collections?: IChakraSelect[] } +interface IFamilyFormIntlAgreements extends IFamilyFormBase { + // Intl. agreements + author?: string + author_type?: string +} + +interface IFamilyFormLawsAndPolicies extends IFamilyFormBase { + // Laws and Policies + topic?: IChakraSelect[] + hazard?: IChakraSelect[] + sector?: IChakraSelect[] + keyword?: IChakraSelect[] + framework?: IChakraSelect[] + instrument?: IChakraSelect[] +} + +type TFamilyFormSubmit = IFamilyFormIntlAgreements | IFamilyFormLawsAndPolicies + type TChildEntity = 'event' | 'document' export const FamilyForm: React.FC = ({ @@ -174,43 +195,76 @@ export const FamilyForm: React.FC = ({ } }, [loadedFamily]) - const handleFormSubmission = async (formData: IFamilyFormBase) => { + // Type-safe metadata handler type + type MetadataHandler = { + extractMetadata: (formData: TFamilyFormSubmit) => T + createSubmissionData: ( + baseData: IFamilyFormPostBase, + metadata: T, + ) => TFamilyFormPost + } + + // Mapping of corpus types to their specific metadata handlers + const corpusMetadataHandlers: Record< + string, + MetadataHandler + > = { + 'Intl. agreements': { + extractMetadata: (formData: TFamilyFormSubmit) => { + const intlData = formData as IFamilyFormIntlAgreements + return { + author: intlData.author ? [intlData.author] : [], + author_type: intlData.author_type ? [intlData.author_type] : [], + } as IInternationalAgreementsMetadata + }, + createSubmissionData: (baseData, metadata) => + ({ + ...baseData, + metadata, + }) as IInternationalAgreementsFamilyFormPost, + }, + 'Laws and Policies': { + extractMetadata: (formData: TFamilyFormSubmit) => { + const lawsPolicyData = formData as IFamilyFormLawsAndPolicies + return { + topic: lawsPolicyData.topic?.map((topic) => topic.value) || [], + hazard: lawsPolicyData.hazard?.map((hazard) => hazard.value) || [], + sector: lawsPolicyData.sector?.map((sector) => sector.value) || [], + keyword: + lawsPolicyData.keyword?.map((keyword) => keyword.value) || [], + framework: + lawsPolicyData.framework?.map((framework) => framework.value) || [], + instrument: + lawsPolicyData.instrument?.map((instrument) => instrument.value) || + [], + } as ILawsAndPoliciesMetadata + }, + createSubmissionData: (baseData, metadata) => + ({ + ...baseData, + metadata, + }) as ILawsAndPoliciesFamilyFormPost, + }, + // Add other corpus types here with their specific metadata extraction logic + } + + const handleFormSubmission = async (formData: TFamilyFormSubmit) => { setIsFormSubmitting(true) setFormError(null) - // Dynamically generate metadata based on corpus type - const familyMetadata = {} as TFamilyFormPostMetadata - - // Handle International Agreements metadata - if (corpusInfo?.corpus_type === 'Intl. agreements') { - const intlAgreementsMetadata: IInternationalAgreementsMetadata = { - author: formData.author ? [formData.author] : [], - author_type: formData.author_type ? [formData.author_type] : [], - } - Object.assign(familyMetadata, intlAgreementsMetadata) + // Validate corpus type + if (!corpusInfo?.corpus_type) { + throw new Error('No corpus type specified') } - // Handle Laws and Policies metadata - else if (corpusInfo?.corpus_type === 'Laws and Policies') { - const lawsPoliciesMetadata: ILawsAndPoliciesMetadata = { - topic: formData.topic?.map((topic) => topic.value as string) || [], - hazard: formData.hazard?.map((hazard) => hazard.value as string) || [], - sector: formData.sector?.map((sector) => sector.value as string) || [], - keyword: - formData.keyword?.map((keyword) => keyword.value as string) || [], - framework: - formData.framework?.map((framework) => framework.value as string) || - [], - instrument: - formData.instrument?.map( - (instrument) => instrument.value as string, - ) || [], - } - Object.assign(familyMetadata, lawsPoliciesMetadata) + // Get the appropriate metadata handler + const metadataHandler = corpusMetadataHandlers[corpusInfo.corpus_type] + if (!metadataHandler) { + throw new Error(`Unsupported corpus type: ${corpusInfo.corpus_type}`) } - // Prepare submission data - const submissionData: TFamilyFormPost = { + // Prepare base family data common to all types + const baseData: IFamilyFormPostBase = { title: formData.title, summary: formData.summary, geography: formData.geography?.value || '', @@ -218,9 +272,17 @@ export const FamilyForm: React.FC = ({ corpus_import_id: formData.corpus?.value || '', collections: formData.collections?.map((collection) => collection.value) || [], - metadata: familyMetadata, } + // Extract metadata + const metadata = metadataHandler.extractMetadata(formData) + + // Create submission data using the specific handler + const submissionData = metadataHandler.createSubmissionData( + baseData, + metadata, + ) + try { if (loadedFamily) { await updateFamily(submissionData, loadedFamily.import_id) diff --git a/src/interfaces/Family.ts b/src/interfaces/Family.ts index 8bb96fe..6b7af1b 100644 --- a/src/interfaces/Family.ts +++ b/src/interfaces/Family.ts @@ -46,7 +46,7 @@ export interface ILawsAndPoliciesFamily extends IFamilyBase { export type TFamily = IInternationalAgreementsFamily | ILawsAndPoliciesFamily // DTO for Create and Write. -interface IFamilyFormPostBase { +export interface IFamilyFormPostBase { title: string summary: string geography: string @@ -68,4 +68,6 @@ export type TFamilyFormPostMetadata = | IInternationalAgreementsMetadata | ILawsAndPoliciesMetadata -export type TFamilyFormPost = IFamilyFormPostBase +export type TFamilyFormPost = + | ILawsAndPoliciesFamilyFormPost + | IInternationalAgreementsFamilyFormPost diff --git a/src/schemas/familySchema.ts b/src/schemas/familySchema.ts index 20a5622..353caba 100644 --- a/src/schemas/familySchema.ts +++ b/src/schemas/familySchema.ts @@ -1,20 +1,5 @@ -import { IChakraSelect } from '@/interfaces' import * as yup from 'yup' -interface IFamilyFormMetadata { - // Intl. agreements - author?: string - author_type?: string - - // Laws and Policies - topic?: IChakraSelect[] - hazard?: IChakraSelect[] - sector?: IChakraSelect[] - keyword?: IChakraSelect[] - framework?: IChakraSelect[] - instrument?: IChakraSelect[] -} - // Base schema for core family fields (non-metadata) export const baseFamilySchema = yup .object({ @@ -36,8 +21,6 @@ export const baseFamilySchema = yup .required() // Function to merge base schema with dynamic metadata schema -export const createFamilySchema = ( - metadataSchema: yup.ObjectSchema, -) => { +export const createFamilySchema = (metadataSchema: yup.ObjectSchema) => { return baseFamilySchema.concat(metadataSchema) } From 98594250f2fbdeff5e86792d066ac699e154c642 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:52:12 +0000 Subject: [PATCH 36/88] Make family form more type safe --- src/components/forms/FamilyForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index f6df919..21db635 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -310,7 +310,7 @@ export const FamilyForm: React.FC = ({ } } - const onSubmit: SubmitHandler = async (data) => { + const onSubmit: SubmitHandler = async (data) => { try { await handleFormSubmission(data) } catch (error) { From fd23a70f817bc292fa06308e425e18de3d302430 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:30:42 +0000 Subject: [PATCH 37/88] Refactor --- .../{forms => family}/ReadOnlyFields.tsx | 0 src/components/forms/FamilyForm.tsx | 115 +++++++----------- .../metadata/familyFormMetadataHandlers.ts | 79 ++++++++++++ 3 files changed, 121 insertions(+), 73 deletions(-) rename src/components/{forms => family}/ReadOnlyFields.tsx (100%) create mode 100644 src/generics/metadata/familyFormMetadataHandlers.ts diff --git a/src/components/forms/ReadOnlyFields.tsx b/src/components/family/ReadOnlyFields.tsx similarity index 100% rename from src/components/forms/ReadOnlyFields.tsx rename to src/components/family/ReadOnlyFields.tsx diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 21db635..6095554 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -23,7 +23,7 @@ import { MetadataSection } from './sections/MetadataSection' import { DocumentSection } from './sections/DocumentSection' import { EventSection } from './sections/EventSection' import { UnsavedChangesModal } from './modals/UnsavedChangesModal' -import { ReadOnlyFields } from './ReadOnlyFields' +import { ReadOnlyFields } from '../family/ReadOnlyFields' import { EntityEditDrawer } from '../drawers/EntityEditDrawer' import { @@ -51,6 +51,7 @@ import { IDocument } from '@/interfaces/Document' import { IEvent } from '@/interfaces/Event' import { IError } from '@/interfaces/Auth' import { IChakraSelect, IConfigCorpora, TTaxonomy } from '@/interfaces' +import { getMetadataHandler } from '../../generics/metadata/familyFormMetadataHandlers' interface FamilyFormProps { family?: TFamily @@ -65,13 +66,13 @@ interface IFamilyFormBase { collections?: IChakraSelect[] } -interface IFamilyFormIntlAgreements extends IFamilyFormBase { +export interface IFamilyFormIntlAgreements extends IFamilyFormBase { // Intl. agreements author?: string author_type?: string } -interface IFamilyFormLawsAndPolicies extends IFamilyFormBase { +export interface IFamilyFormLawsAndPolicies extends IFamilyFormBase { // Laws and Policies topic?: IChakraSelect[] hazard?: IChakraSelect[] @@ -81,7 +82,9 @@ interface IFamilyFormLawsAndPolicies extends IFamilyFormBase { instrument?: IChakraSelect[] } -type TFamilyFormSubmit = IFamilyFormIntlAgreements | IFamilyFormLawsAndPolicies +export type TFamilyFormSubmit = + | IFamilyFormIntlAgreements + | IFamilyFormLawsAndPolicies type TChildEntity = 'event' | 'document' @@ -140,7 +143,6 @@ export const FamilyForm: React.FC = ({ setValue, watch, formState: { errors, isSubmitting, dirtyFields }, - trigger, } = useForm({ resolver: yupResolver(validationSchema), }) @@ -154,13 +156,6 @@ export const FamilyForm: React.FC = ({ ) const taxonomy = corpusInfo?.taxonomy - // Update validation schema when corpus/taxonomy changes - useEffect(() => { - createValidationSchema(taxonomy, corpusInfo) - // Re-trigger form validation with new schema - trigger() - }, [taxonomy, corpusInfo, createValidationSchema, trigger]) - // Determine if the corpus is an MCF type const isMCFCorpus = useMemo(() => { return ( @@ -195,59 +190,6 @@ export const FamilyForm: React.FC = ({ } }, [loadedFamily]) - // Type-safe metadata handler type - type MetadataHandler = { - extractMetadata: (formData: TFamilyFormSubmit) => T - createSubmissionData: ( - baseData: IFamilyFormPostBase, - metadata: T, - ) => TFamilyFormPost - } - - // Mapping of corpus types to their specific metadata handlers - const corpusMetadataHandlers: Record< - string, - MetadataHandler - > = { - 'Intl. agreements': { - extractMetadata: (formData: TFamilyFormSubmit) => { - const intlData = formData as IFamilyFormIntlAgreements - return { - author: intlData.author ? [intlData.author] : [], - author_type: intlData.author_type ? [intlData.author_type] : [], - } as IInternationalAgreementsMetadata - }, - createSubmissionData: (baseData, metadata) => - ({ - ...baseData, - metadata, - }) as IInternationalAgreementsFamilyFormPost, - }, - 'Laws and Policies': { - extractMetadata: (formData: TFamilyFormSubmit) => { - const lawsPolicyData = formData as IFamilyFormLawsAndPolicies - return { - topic: lawsPolicyData.topic?.map((topic) => topic.value) || [], - hazard: lawsPolicyData.hazard?.map((hazard) => hazard.value) || [], - sector: lawsPolicyData.sector?.map((sector) => sector.value) || [], - keyword: - lawsPolicyData.keyword?.map((keyword) => keyword.value) || [], - framework: - lawsPolicyData.framework?.map((framework) => framework.value) || [], - instrument: - lawsPolicyData.instrument?.map((instrument) => instrument.value) || - [], - } as ILawsAndPoliciesMetadata - }, - createSubmissionData: (baseData, metadata) => - ({ - ...baseData, - metadata, - }) as ILawsAndPoliciesFamilyFormPost, - }, - // Add other corpus types here with their specific metadata extraction logic - } - const handleFormSubmission = async (formData: TFamilyFormSubmit) => { setIsFormSubmitting(true) setFormError(null) @@ -258,10 +200,7 @@ export const FamilyForm: React.FC = ({ } // Get the appropriate metadata handler - const metadataHandler = corpusMetadataHandlers[corpusInfo.corpus_type] - if (!metadataHandler) { - throw new Error(`Unsupported corpus type: ${corpusInfo.corpus_type}`) - } + const metadataHandler = getMetadataHandler(corpusInfo.corpus_type) // Prepare base family data common to all types const baseData: IFamilyFormPostBase = { @@ -315,12 +254,42 @@ export const FamilyForm: React.FC = ({ await handleFormSubmission(data) } catch (error) { console.log('onSubmitErrorHandler', error) + // Handle any submission errors + setFormError(error as IError) + toast({ + title: 'Submission Error', + description: (error as IError).message, + status: 'error', + }) } } - const onSubmitErrorHandler = (error: object) => { - console.log('onSubmitErrorHandler', error) - } + useEffect(() => { + if (loadedFamily) { + reset({ + title: loadedFamily.title, + summary: loadedFamily.summary, + geography: { + value: loadedFamily.geography, + label: + getCountries()?.find( + (country) => country.value === loadedFamily.geography, + )?.display_value || loadedFamily.geography, + }, + corpus: loadedFamily.corpus_import_id + ? { + label: loadedFamily.corpus_import_id, + value: loadedFamily.corpus_import_id, + } + : undefined, + category: loadedFamily.category, + collections: loadedFamily.collections?.map((collection) => ({ + value: collection, + label: collection, + })), + }) + } + }, [loadedFamily, reset]) const onAddNewEntityClick = (entityType: TChildEntity) => { setEditingEntity(entityType) @@ -504,7 +473,7 @@ export const FamilyForm: React.FC = ({ )} {canLoadForm && ( -
+ {formError && } diff --git a/src/generics/metadata/familyFormMetadataHandlers.ts b/src/generics/metadata/familyFormMetadataHandlers.ts new file mode 100644 index 0000000..90abbe2 --- /dev/null +++ b/src/generics/metadata/familyFormMetadataHandlers.ts @@ -0,0 +1,79 @@ +import { + IFamilyFormIntlAgreements, + IFamilyFormLawsAndPolicies, + TFamilyFormSubmit, +} from '@/components/forms/FamilyForm' +import { + IFamilyFormPostBase, + IInternationalAgreementsMetadata, + ILawsAndPoliciesMetadata, + TFamilyFormPost, + TFamilyFormPostMetadata, +} from '../../interfaces/Family' +import { + IInternationalAgreementsFamilyFormPost, + ILawsAndPoliciesFamilyFormPost, +} from '../../interfaces/Family' + +// Type-safe metadata handler type +export type MetadataHandler = { + extractMetadata: (formData: TFamilyFormSubmit) => T + createSubmissionData: ( + baseData: IFamilyFormPostBase, + metadata: T, + ) => TFamilyFormPost +} + +// Mapping of corpus types to their specific metadata handlers +export const corpusMetadataHandlers: Record< + string, + MetadataHandler +> = { + 'Intl. agreements': { + extractMetadata: (formData: TFamilyFormSubmit) => { + const intlData = formData as IFamilyFormIntlAgreements + return { + author: intlData.author ? [intlData.author] : [], + author_type: intlData.author_type ? [intlData.author_type] : [], + } as IInternationalAgreementsMetadata + }, + createSubmissionData: (baseData, metadata) => + ({ + ...baseData, + metadata, + }) as IInternationalAgreementsFamilyFormPost, + }, + 'Laws and Policies': { + extractMetadata: (formData: TFamilyFormSubmit) => { + const lawsPolicyData = formData as IFamilyFormLawsAndPolicies + return { + topic: lawsPolicyData.topic?.map((topic) => topic.value) || [], + hazard: lawsPolicyData.hazard?.map((hazard) => hazard.value) || [], + sector: lawsPolicyData.sector?.map((sector) => sector.value) || [], + keyword: lawsPolicyData.keyword?.map((keyword) => keyword.value) || [], + framework: + lawsPolicyData.framework?.map((framework) => framework.value) || [], + instrument: + lawsPolicyData.instrument?.map((instrument) => instrument.value) || + [], + } as ILawsAndPoliciesMetadata + }, + createSubmissionData: (baseData, metadata) => + ({ + ...baseData, + metadata, + }) as ILawsAndPoliciesFamilyFormPost, + }, + // Add other corpus types here with their specific metadata extraction logic +} + +// Utility function to get metadata handler for a specific corpus type +export const getMetadataHandler = ( + corpusType: string, +): MetadataHandler => { + const handler = corpusMetadataHandlers[corpusType] + if (!handler) { + throw new Error(`Unsupported corpus type: ${corpusType}`) + } + return handler +} From 5518306ce35cd23340d2d583474dde9a2c237f20 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:30:44 +0000 Subject: [PATCH 38/88] Refactor --- src/components/forms/FamilyForm.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 6095554..01719a2 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -28,13 +28,8 @@ import { EntityEditDrawer } from '../drawers/EntityEditDrawer' import { TFamilyFormPost, - IInternationalAgreementsMetadata, - ILawsAndPoliciesMetadata, - TFamilyFormPostMetadata, TFamily, IFamilyFormPostBase, - IInternationalAgreementsFamilyFormPost, - ILawsAndPoliciesFamilyFormPost, } from '@/interfaces/Family' import { canModify } from '@/utils/canModify' import { getCountries } from '@/utils/extractNestedGeographyData' @@ -46,7 +41,6 @@ import { deleteDocument } from '@/api/Documents' import { deleteEvent } from '@/api/Events' import { createFamilySchema } from '@/schemas/familySchema' import { ApiError } from '../feedback/ApiError' -// import { IChakraSelect } from '@/interfaces/Config' import { IDocument } from '@/interfaces/Document' import { IEvent } from '@/interfaces/Event' import { IError } from '@/interfaces/Auth' From a97c7fd77f6a2f9b42b74afcdcb3ceec434d4968 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:35:23 +0000 Subject: [PATCH 39/88] Fix definition --- src/components/forms/EventForm.tsx | 10 +++++----- src/components/forms/FamilyForm.tsx | 12 +++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/forms/EventForm.tsx b/src/components/forms/EventForm.tsx index 39d0ee9..95811ee 100644 --- a/src/components/forms/EventForm.tsx +++ b/src/components/forms/EventForm.tsx @@ -26,16 +26,16 @@ import { import { formatDateISO } from '@/utils/formatDate' import { ApiError } from '../feedback/ApiError' -type TaxonomyEventType = - | { event_type: string[] } - | { event_type: { allowed_values: string[] } } - | undefined +// type TaxonomyEventType = +// | { event_type: string[] } +// | { event_type: { allowed_values: string[] } } +// | undefined type TProps = { familyId?: string canModify?: boolean event?: IEvent - taxonomy?: IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC | TaxonomyEventType + taxonomy?: IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC // | TaxonomyEventType onSuccess?: (eventId: string) => void } diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 01719a2..33cb07c 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -47,10 +47,6 @@ import { IError } from '@/interfaces/Auth' import { IChakraSelect, IConfigCorpora, TTaxonomy } from '@/interfaces' import { getMetadataHandler } from '../../generics/metadata/familyFormMetadataHandlers' -interface FamilyFormProps { - family?: TFamily -} - interface IFamilyFormBase { title: string summary: string @@ -82,9 +78,11 @@ export type TFamilyFormSubmit = type TChildEntity = 'event' | 'document' -export const FamilyForm: React.FC = ({ - family: loadedFamily, -}) => { +type TProps = { + family?: TFamily +} + +export const FamilyForm = ({ family: loadedFamily }: TProps) => { const [isLeavingModalOpen, setIsLeavingModalOpen] = useState(false) const [isFormSubmitting, setIsFormSubmitting] = useState(false) const { isOpen, onOpen, onClose } = useDisclosure() From 2d06017c51242bef54514d41c2872f2f749bccfa Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:53:07 +0000 Subject: [PATCH 40/88] Type errors --- src/components/forms/FamilyForm.tsx | 70 +++++------------------------ 1 file changed, 10 insertions(+), 60 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 33cb07c..d2ef55b 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -26,11 +26,7 @@ import { UnsavedChangesModal } from './modals/UnsavedChangesModal' import { ReadOnlyFields } from '../family/ReadOnlyFields' import { EntityEditDrawer } from '../drawers/EntityEditDrawer' -import { - TFamilyFormPost, - TFamily, - IFamilyFormPostBase, -} from '@/interfaces/Family' +import { TFamily, IFamilyFormPostBase } from '@/interfaces/Family' import { canModify } from '@/utils/canModify' import { getCountries } from '@/utils/extractNestedGeographyData' import { decodeToken } from '@/utils/decodeToken' @@ -191,9 +187,6 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { throw new Error('No corpus type specified') } - // Get the appropriate metadata handler - const metadataHandler = getMetadataHandler(corpusInfo.corpus_type) - // Prepare base family data common to all types const baseData: IFamilyFormPostBase = { title: formData.title, @@ -205,7 +198,8 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { formData.collections?.map((collection) => collection.value) || [], } - // Extract metadata + // Get the appropriate metadata handler & extract metadata + const metadataHandler = getMetadataHandler(corpusInfo.corpus_type) const metadata = metadataHandler.extractMetadata(formData) // Create submission data using the specific handler @@ -258,6 +252,10 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { useEffect(() => { if (loadedFamily) { + setFamilyDocuments(loadedFamily.documents || []) + setFamilyEvents(loadedFamily.events || []) + + // Pre-set the form values to that of the loaded family reset({ title: loadedFamily.title, summary: loadedFamily.summary, @@ -274,14 +272,14 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { value: loadedFamily.corpus_import_id, } : undefined, - category: loadedFamily.category, + category: isMCFCorpus ? 'MCF' : loadedFamily.category, collections: loadedFamily.collections?.map((collection) => ({ value: collection, label: collection, })), }) } - }, [loadedFamily, reset]) + }, [loadedFamily, reset, isMCFCorpus]) const onAddNewEntityClick = (entityType: TChildEntity) => { setEditingEntity(entityType) @@ -380,55 +378,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { const canLoadForm = !configLoading && !collectionsLoading && !configError && !collectionsError - useEffect(() => { - if (loadedFamily && collections) { - setFamilyDocuments(loadedFamily.documents || []) - setFamilyEvents(loadedFamily.events || []) - - const resetValues: Partial = { - title: loadedFamily.title, - summary: loadedFamily.summary, - collections: - loadedFamily.collections - ?.map((collectionId) => { - const collection = collections.find( - (c) => c.import_id === collectionId, - ) - return collection - ? { - value: collection.import_id, - label: collection.title, - } - : null - }) - .filter(Boolean) || [], - geography: loadedFamily.geography - ? { - value: loadedFamily.geography, - label: - getCountries(config?.geographies).find( - (country) => country.value === loadedFamily.geography, - )?.display_value || loadedFamily.geography, - } - : undefined, - corpus: loadedFamily.corpus_import_id - ? { - label: loadedFamily.corpus_import_id, - value: loadedFamily.corpus_import_id, - } - : undefined, - } - - // Set category to MCF for MCF corpora - if (isMCFCorpus) { - resetValues.category = 'MCF' - } else { - resetValues.category = loadedFamily.category - } - - reset(resetValues) - } - }, [loadedFamily, collections, reset, isMCFCorpus]) + // }, [loadedFamily, collections, reset, isMCFCorpus]) const blocker = useBlocker( ({ currentLocation, nextLocation }) => From e6815c74b0aea1e1c84cfeead85613ce2951965d Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:57:26 +0000 Subject: [PATCH 41/88] Type errors --- src/components/forms/FamilyForm.tsx | 2 +- .../forms/metadata-handlers/familyForm.ts} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/{generics/metadata/familyFormMetadataHandlers.ts => components/forms/metadata-handlers/familyForm.ts} (97%) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index d2ef55b..350c42d 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -41,7 +41,7 @@ import { IDocument } from '@/interfaces/Document' import { IEvent } from '@/interfaces/Event' import { IError } from '@/interfaces/Auth' import { IChakraSelect, IConfigCorpora, TTaxonomy } from '@/interfaces' -import { getMetadataHandler } from '../../generics/metadata/familyFormMetadataHandlers' +import { getMetadataHandler } from './metadata-handlers/familyForm' interface IFamilyFormBase { title: string diff --git a/src/generics/metadata/familyFormMetadataHandlers.ts b/src/components/forms/metadata-handlers/familyForm.ts similarity index 97% rename from src/generics/metadata/familyFormMetadataHandlers.ts rename to src/components/forms/metadata-handlers/familyForm.ts index 90abbe2..5567dc0 100644 --- a/src/generics/metadata/familyFormMetadataHandlers.ts +++ b/src/components/forms/metadata-handlers/familyForm.ts @@ -9,11 +9,11 @@ import { ILawsAndPoliciesMetadata, TFamilyFormPost, TFamilyFormPostMetadata, -} from '../../interfaces/Family' +} from '../../../interfaces/Family' import { IInternationalAgreementsFamilyFormPost, ILawsAndPoliciesFamilyFormPost, -} from '../../interfaces/Family' +} from '../../../interfaces/Family' // Type-safe metadata handler type export type MetadataHandler = { From 57527cbd058c9a5d8226081f80397b5aacdae7e0 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:14:18 +0000 Subject: [PATCH 42/88] Type errors --- src/components/forms/FamilyForm.tsx | 4 +- .../forms/fields/RadioGroupField.tsx | 8 +- src/components/forms/fields/SelectField.tsx | 6 +- src/interfaces/Config.ts | 5 - src/interfaces/Metadata.ts | 5 - src/schemas/dynamicValidationSchema.ts | 148 +++++++++++------- src/utils/metadataUtils.ts | 4 +- src/views/family/Families.tsx | 3 +- 8 files changed, 103 insertions(+), 80 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 350c42d..ebd40c6 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -262,7 +262,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { geography: { value: loadedFamily.geography, label: - getCountries()?.find( + getCountries(config?.geographies)?.find( (country) => country.value === loadedFamily.geography, )?.display_value || loadedFamily.geography, }, @@ -279,7 +279,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { })), }) } - }, [loadedFamily, reset, isMCFCorpus]) + }, [config, loadedFamily, reset, isMCFCorpus]) const onAddNewEntityClick = (entityType: TChildEntity) => { setEditingEntity(entityType) diff --git a/src/components/forms/fields/RadioGroupField.tsx b/src/components/forms/fields/RadioGroupField.tsx index 2428602..3869547 100644 --- a/src/components/forms/fields/RadioGroupField.tsx +++ b/src/components/forms/fields/RadioGroupField.tsx @@ -14,17 +14,13 @@ import { Radio, HStack, } from '@chakra-ui/react' - -interface Option { - value: string - label: string -} +import { IChakraSelect } from '@/interfaces' interface RadioGroupFieldProps { name: Path label: string control: Control - options: Option[] + options: IChakraSelect[] rules?: RegisterOptions } diff --git a/src/components/forms/fields/SelectField.tsx b/src/components/forms/fields/SelectField.tsx index da7a2a2..7121719 100644 --- a/src/components/forms/fields/SelectField.tsx +++ b/src/components/forms/fields/SelectField.tsx @@ -2,15 +2,15 @@ import React from 'react' import { Controller, Control, RegisterOptions } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' import { chakraStylesSelect } from '@/styles/chakra' -import { SelectOption } from '@/interfaces/Metadata' import { generateSelectOptions } from '@/utils/metadataUtils' import { FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react' +import { IChakraSelect } from '@/interfaces' interface SelectFieldProps> { name: string label?: string control: Control - options: string[] | SelectOption[] + options: string[] | IChakraSelect[] isMulti?: boolean rules?: RegisterOptions isRequired?: boolean @@ -25,7 +25,7 @@ export const SelectField = >({ rules, isRequired, }: SelectFieldProps): React.ReactElement => { - // Determine if options are already in SelectOption format + // Determine if options are already in IChakraSelect format const selectOptions = options ? Array.isArray(options) && options.length > 0 && diff --git a/src/interfaces/Config.ts b/src/interfaces/Config.ts index 92fcb63..9408b76 100644 --- a/src/interfaces/Config.ts +++ b/src/interfaces/Config.ts @@ -23,11 +23,6 @@ export interface IConfigLanguageSorted extends IChakraSelect {} export interface IConfigCorpus extends IChakraSelect {} -export interface IConfigLanguageSorted extends OptionBase { - value: string - label: string -} - // Types for taxonomy and corpus info export interface ITaxonomyField { allowed_values?: string[] diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index e2d29ae..ad60128 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -28,11 +28,6 @@ export interface CorpusMetadataConfig { } } -export interface SelectOption { - value: string - label: string -} - export interface DynamicMetadataFieldProps> { fieldKey: string taxonomyField: ITaxonomyField diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index f6faa35..710010d 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -3,22 +3,102 @@ import { FieldType, CorpusInfo, CORPUS_METADATA_CONFIG, + MetadataFieldConfig, } from '@/interfaces/Metadata' import { TTaxonomy } from '@/interfaces' +import { IChakraSelect, ITaxonomyField } from '@/interfaces/Config' +// Strongly typed validation schema creator +type ValidationSchema = yup.Schema + +// Type-safe field validation function +const getFieldValidation = >( + fieldConfig: MetadataFieldConfig, + fieldKey: string, + isRequired: boolean, + taxonomyField?: ITaxonomyField, +): ValidationSchema => { + let fieldValidation: ValidationSchema + + switch (fieldConfig.type) { + case FieldType.MULTI_SELECT: + fieldValidation = yup.array().of( + yup.object({ + value: yup.string(), + label: yup.string(), + }), + ) as ValidationSchema + break + case FieldType.SINGLE_SELECT: + fieldValidation = yup.string() as ValidationSchema + break + case FieldType.TEXT: + fieldValidation = yup.string() as ValidationSchema + break + case FieldType.NUMBER: + fieldValidation = yup.number() as ValidationSchema + break + case FieldType.DATE: + fieldValidation = yup.date() as ValidationSchema + break + default: + fieldValidation = yup.mixed() as ValidationSchema + } + + // Add required validation if needed + if (isRequired) { + fieldValidation = fieldValidation.required( + `${fieldKey} is required`, + ) as ValidationSchema + } + + // Add allowed values validation if specified in taxonomy + if (taxonomyField?.allowed_values && !taxonomyField.allow_any) { + if (fieldConfig.type === FieldType.MULTI_SELECT) { + fieldValidation = fieldValidation.test( + 'allowed-values', + `${fieldKey} contains invalid values`, + (value: IChakraSelect[]) => { + if (!value) return true + return value.every((item) => + taxonomyField.allowed_values?.includes(item.value), + ) + }, + ) + } else { + // Convert allowed_values to a type that Yup's oneOf can accept + const allowedValues = (taxonomyField.allowed_values || []) as T[keyof T][] + + fieldValidation = fieldValidation.oneOf( + allowedValues, + `${fieldKey} must be one of the allowed values`, + ) + } + } + + return fieldValidation +} + +// Strongly typed dynamic validation schema generator export const generateDynamicValidationSchema = ( taxonomy?: TTaxonomy, corpusInfo?: CorpusInfo, ): yup.ObjectSchema => { + // Early return if no taxonomy or corpus info if (!taxonomy || !corpusInfo) { return yup.object({}).required() as yup.ObjectSchema } + // Get metadata fields and validation fields for the specific corpus type const metadataFields = - CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields || {} + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields || + CORPUS_METADATA_CONFIG.default.renderFields + const validationFields = - CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.validationFields || [] + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.validationFields || + CORPUS_METADATA_CONFIG.default.validationFields + // Build schema shape dynamically const schemaShape = Object.entries(metadataFields).reduce( (acc, [fieldKey, fieldConfig]) => { // Get the field's taxonomy configuration @@ -27,66 +107,22 @@ export const generateDynamicValidationSchema = ( validationFields.includes(fieldKey) && (!taxonomyField || taxonomyField.allow_blanks === false) - // Generate field validation based on field type and requirements - let fieldValidation: yup.Schema - switch (fieldConfig.type) { - case FieldType.MULTI_SELECT: - fieldValidation = yup.array().of( - yup.object({ - value: yup.string(), - label: yup.string(), - }), - ) - break - case FieldType.SINGLE_SELECT: - fieldValidation = yup.string() - break - case FieldType.TEXT: - fieldValidation = yup.string() - break - case FieldType.NUMBER: - fieldValidation = yup.number() - break - case FieldType.DATE: - fieldValidation = yup.date() - break - default: - fieldValidation = yup.mixed() - } - - // Add required validation if needed - if (isRequired) { - fieldValidation = fieldValidation.required(`${fieldKey} is required`) - } - - // Add allowed values validation if specified in taxonomy - if (taxonomyField?.allowed_values && !taxonomyField.allow_any) { - if (fieldConfig.type === FieldType.MULTI_SELECT) { - fieldValidation = fieldValidation.test( - 'allowed-values', - `${fieldKey} contains invalid values`, - (value) => { - if (!value) return true - return value.every((item: any) => - taxonomyField.allowed_values?.includes(item.value), - ) - }, - ) - } else { - fieldValidation = fieldValidation.oneOf( - taxonomyField.allowed_values, - `${fieldKey} must be one of the allowed values`, - ) - } - } + // Generate field validation + const fieldValidation = getFieldValidation( + fieldConfig, + fieldKey, + isRequired, + taxonomyField, + ) return { ...acc, [fieldKey]: fieldValidation, } }, - {} as T, + {} as Partial, ) - return yup.object(schemaShape).required() + // Create and return the final schema + return yup.object(schemaShape).required() as yup.ObjectSchema } diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index e9edb2f..0342bb4 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -1,4 +1,4 @@ -import { SelectOption } from '@/interfaces/Metadata' +import { IChakraSelect } from '@/interfaces' export const formatFieldLabel = (key: string): string => { return key @@ -7,7 +7,7 @@ export const formatFieldLabel = (key: string): string => { .join(' ') } -export const generateSelectOptions = (values?: string[]): SelectOption[] => { +export const generateSelectOptions = (values?: string[]): IChakraSelect[] => { if (!values) return [] return values.map((value) => ({ value, label: value })) } diff --git a/src/views/family/Families.tsx b/src/views/family/Families.tsx index b8a3eb8..ac08af1 100644 --- a/src/views/family/Families.tsx +++ b/src/views/family/Families.tsx @@ -26,6 +26,7 @@ import { SearchIcon } from '@chakra-ui/icons' import useConfig from '@/hooks/useConfig' import { getCountries } from '@/utils/extractNestedGeographyData' import { chakraStylesSelect } from '@/styles/chakra' +import { IChakraSelect } from '@/interfaces' export default function Families() { const navigation = useNavigation() @@ -39,7 +40,7 @@ export default function Families() { }) const handleChangeGeo = (newValue: unknown) => { - const selectedItem = newValue as { value: string; label: string } + const selectedItem = newValue as IChakraSelect setSearchParams({ geography: selectedItem?.value ?? '', q: searchParams.get('q') ?? '', From e0e55f59b1f5b0f71ce2b39d638f0661457aa9e8 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:58:07 +0000 Subject: [PATCH 43/88] Update types --- src/interfaces/Config.ts | 9 +-------- src/interfaces/Family.ts | 6 ++++-- src/interfaces/Metadata.ts | 15 ++++----------- src/interfaces/index.ts | 8 +++++++- src/schemas/dynamicValidationSchema.ts | 4 ++-- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/interfaces/Config.ts b/src/interfaces/Config.ts index 9408b76..ef60780 100644 --- a/src/interfaces/Config.ts +++ b/src/interfaces/Config.ts @@ -1,4 +1,4 @@ -import { OptionBase } from 'chakra-react-select' +import { IChakraSelect } from '.' export interface IConfigGeographyNode { id: number @@ -14,15 +14,8 @@ export interface IConfigGeography { children: IConfigGeography[] } -export interface IChakraSelect extends OptionBase { - value: string - label: string -} - export interface IConfigLanguageSorted extends IChakraSelect {} -export interface IConfigCorpus extends IChakraSelect {} - // Types for taxonomy and corpus info export interface ITaxonomyField { allowed_values?: string[] diff --git a/src/interfaces/Family.ts b/src/interfaces/Family.ts index 6b7af1b..c875a72 100644 --- a/src/interfaces/Family.ts +++ b/src/interfaces/Family.ts @@ -1,10 +1,12 @@ +import { IMetadata } from './Metadata' + // Corpus type metadata (aka taxonomy). -export interface IInternationalAgreementsMetadata { +export interface IInternationalAgreementsMetadata extends IMetadata { author: string[] author_type: string[] } -export interface ILawsAndPoliciesMetadata { +export interface ILawsAndPoliciesMetadata extends IMetadata { topic: string[] hazard: string[] sector: string[] diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index ad60128..9ee0d5b 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -1,6 +1,3 @@ -import { Control, FieldErrors } from 'react-hook-form' -import { ITaxonomyField } from './Config' - export enum FieldType { TEXT = 'text', MULTI_SELECT = 'multi_select', @@ -9,6 +6,10 @@ export enum FieldType { DATE = 'date', } +export interface IMetadata { + [key: string]: string[] +} + export interface MetadataFieldConfig { type: FieldType label?: string @@ -28,14 +29,6 @@ export interface CorpusMetadataConfig { } } -export interface DynamicMetadataFieldProps> { - fieldKey: string - taxonomyField: ITaxonomyField - control: Control - errors: FieldErrors - fieldType: FieldType -} - // Centralised configuration for corpus metadata export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { 'Intl. agreements': { diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index d54f42c..1647206 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -5,5 +5,11 @@ export * from './Corpus' export * from './Document' export * from './Event' export * from './Family' -export * from './Organisation' export * from './Summary' + +import { OptionBase } from 'chakra-react-select' + +export interface IChakraSelect extends OptionBase { + value: string + label: string +} diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 710010d..6f4388b 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -5,8 +5,8 @@ import { CORPUS_METADATA_CONFIG, MetadataFieldConfig, } from '@/interfaces/Metadata' -import { TTaxonomy } from '@/interfaces' -import { IChakraSelect, ITaxonomyField } from '@/interfaces/Config' +import { IChakraSelect, TTaxonomy } from '@/interfaces' +import { ITaxonomyField } from '@/interfaces/Config' // Strongly typed validation schema creator type ValidationSchema = yup.Schema From 5d189a66a3094c37508f023d509f6ee2b799703e Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:58:52 +0000 Subject: [PATCH 44/88] Update declaration --- .../forms/sections/MetadataSection.tsx | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index db3d95c..38cd38d 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -1,63 +1,62 @@ -import React, { useEffect } from 'react' -import { Control, FieldErrors, UseFormReset } from 'react-hook-form' +import { useEffect } from 'react' +import { useForm } from 'react-hook-form' import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' import { DynamicMetadataFields } from '../DynamicMetadataFields' import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' -interface MetadataSectionProps> { +type TProps = { corpusInfo?: IConfigCorpora taxonomy?: TTaxonomy - control: Control - errors: FieldErrors loadedFamily?: TFamily - reset: UseFormReset } -export const MetadataSection: React.FC> = ({ +export const MetadataSection = ({ corpusInfo, taxonomy, - control, - errors, loadedFamily, - reset, -}) => { +}: TProps) => { + const { + control, + reset, + formState: { errors }, + } = useForm() + useEffect(() => { if (loadedFamily?.metadata && corpusInfo) { - const metadataValues = Object.entries(loadedFamily.metadata).reduce< - Record - >((acc, [key, value]) => { + const metadataValues = Object.entries( + loadedFamily.metadata as Record, + ).reduce>((acc, [fieldKey, value]) => { const fieldConfig = - CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[ + fieldKey + ] if (!fieldConfig) return acc if (fieldConfig.type === FieldType.SINGLE_SELECT) { - acc[key] = value?.[0] + acc[fieldKey] = value?.[0] ? { value: value[0], label: value[0], } : undefined } else if (fieldConfig.type === FieldType.MULTI_SELECT) { - acc[key] = value?.map((v) => ({ + acc[fieldKey] = value?.map((v: string) => ({ value: v, label: v, })) } else { - acc[key] = value + acc[fieldKey] = value } return acc }, {}) - reset((formValues) => ({ - ...formValues, - ...metadataValues, - })) + reset(metadataValues) } }, [loadedFamily, corpusInfo, reset]) - if (!corpusInfo || !taxonomy) return null + if (!corpusInfo || !taxonomy) return <> return ( <> From 3d09a220aa25b06c3a7ee93445a3f48cf7c1a8fc Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:01:58 +0000 Subject: [PATCH 45/88] Type errors --- src/components/forms/EventForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/forms/EventForm.tsx b/src/components/forms/EventForm.tsx index 95811ee..d90471a 100644 --- a/src/components/forms/EventForm.tsx +++ b/src/components/forms/EventForm.tsx @@ -188,12 +188,12 @@ export const EventForm = ({ {/* Add event type options from taxonomy if available */} {taxonomy?.event_type && (Array.isArray(taxonomy.event_type) - ? taxonomy.event_type.map((type) => ( + ? taxonomy.event_type.map((type: string) => ( )) - : taxonomy.event_type.allowed_values?.map((type) => ( + : taxonomy.event_type.allowed_values?.map((type: string) => ( From eab955304e16b904b8616cbb2e02c099e77b8654 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:05:15 +0000 Subject: [PATCH 46/88] Unused imports --- src/components/drawers/DocumentEditDrawer.tsx | 5 ++--- src/components/drawers/EntityEditDrawer.tsx | 5 ++--- src/components/drawers/EventEditDrawer.tsx | 5 ++--- src/components/family/ReadOnlyFields.tsx | 3 +-- src/components/forms/DynamicMetadataFields.tsx | 3 +-- src/components/forms/fields/RadioGroupField.tsx | 1 - src/components/forms/fields/SelectField.tsx | 3 +-- src/components/forms/fields/TextField.tsx | 3 +-- src/components/forms/modals/UnsavedChangesModal.tsx | 5 ++--- src/components/forms/sections/DocumentSection.tsx | 5 ++--- src/components/forms/sections/EventSection.tsx | 5 ++--- 11 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/components/drawers/DocumentEditDrawer.tsx b/src/components/drawers/DocumentEditDrawer.tsx index f11206a..7875b16 100644 --- a/src/components/drawers/DocumentEditDrawer.tsx +++ b/src/components/drawers/DocumentEditDrawer.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Drawer, DrawerBody, @@ -19,7 +18,7 @@ interface DocumentEditDrawerProps { taxonomy?: TTaxonomy } -export const DocumentEditDrawer: React.FC = ({ +export const DocumentEditDrawer = ({ document, familyId, onClose, @@ -27,7 +26,7 @@ export const DocumentEditDrawer: React.FC = ({ onSuccess, canModify, taxonomy, -}) => { +}: DocumentEditDrawerProps) => { return ( diff --git a/src/components/drawers/EntityEditDrawer.tsx b/src/components/drawers/EntityEditDrawer.tsx index 86af593..e7839e2 100644 --- a/src/components/drawers/EntityEditDrawer.tsx +++ b/src/components/drawers/EntityEditDrawer.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { DocumentEditDrawer } from './DocumentEditDrawer' import { EventEditDrawer } from './EventEditDrawer' import { IDocument, IEvent, TTaxonomy } from '@/interfaces' @@ -16,7 +15,7 @@ interface EntityEditDrawerProps { canModify?: boolean } -export const EntityEditDrawer: React.FC = ({ +export const EntityEditDrawer = ({ isOpen, onClose, entity, @@ -27,7 +26,7 @@ export const EntityEditDrawer: React.FC = ({ familyId, taxonomy, canModify, -}) => { +}: EntityEditDrawerProps) => { if (entity === 'document') { return ( = ({ +export const EventEditDrawer = ({ event: loadedEvent, familyId, onClose, @@ -28,7 +27,7 @@ export const EventEditDrawer: React.FC = ({ onSuccess, canModify, taxonomy, -}) => { +}: EventEditDrawerProps) => { return ( diff --git a/src/components/family/ReadOnlyFields.tsx b/src/components/family/ReadOnlyFields.tsx index 5e8063b..932a8b8 100644 --- a/src/components/family/ReadOnlyFields.tsx +++ b/src/components/family/ReadOnlyFields.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Box, Text, VStack, Divider } from '@chakra-ui/react' import { TFamily } from '@/interfaces' @@ -6,7 +5,7 @@ interface ReadOnlyFieldsProps { family: TFamily } -export const ReadOnlyFields: React.FC = ({ family }) => { +export const ReadOnlyFields = ({ family }: ReadOnlyFieldsProps) => { return ( <> diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index 78eaf26..eef62c0 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { FormControl, FormLabel, @@ -30,7 +29,7 @@ export const DynamicMetadataFields = >({ control, errors, fieldType, -}: DynamicMetadataFieldProps): React.ReactElement => { +}: DynamicMetadataFieldProps) => { const { allowed_values = [], allow_any = false, diff --git a/src/components/forms/fields/RadioGroupField.tsx b/src/components/forms/fields/RadioGroupField.tsx index 3869547..c60765b 100644 --- a/src/components/forms/fields/RadioGroupField.tsx +++ b/src/components/forms/fields/RadioGroupField.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Control, Controller, diff --git a/src/components/forms/fields/SelectField.tsx b/src/components/forms/fields/SelectField.tsx index 7121719..d8d47f4 100644 --- a/src/components/forms/fields/SelectField.tsx +++ b/src/components/forms/fields/SelectField.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Controller, Control, RegisterOptions } from 'react-hook-form' import { Select as CRSelect } from 'chakra-react-select' import { chakraStylesSelect } from '@/styles/chakra' @@ -24,7 +23,7 @@ export const SelectField = >({ isMulti = false, rules, isRequired, -}: SelectFieldProps): React.ReactElement => { +}: SelectFieldProps) => { // Determine if options are already in IChakraSelect format const selectOptions = options ? Array.isArray(options) && diff --git a/src/components/forms/fields/TextField.tsx b/src/components/forms/fields/TextField.tsx index 35cada2..6949a9f 100644 --- a/src/components/forms/fields/TextField.tsx +++ b/src/components/forms/fields/TextField.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Controller, Control } from 'react-hook-form' import { Input, @@ -23,7 +22,7 @@ export const TextField = >({ placeholder, label, isRequired, -}: TextFieldProps): React.ReactElement => { +}: TextFieldProps) => { return ( void } -export const UnsavedChangesModal: React.FC = ({ +export const UnsavedChangesModal = ({ isOpen, onClose, onConfirm, -}) => { +}: UnsavedChangesModalProps) => { return ( diff --git a/src/components/forms/sections/DocumentSection.tsx b/src/components/forms/sections/DocumentSection.tsx index 13ad1f2..d4ada2c 100644 --- a/src/components/forms/sections/DocumentSection.tsx +++ b/src/components/forms/sections/DocumentSection.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Box, Button, @@ -22,7 +21,7 @@ interface DocumentSectionProps { isNewFamily: boolean } -export const DocumentSection: React.FC = ({ +export const DocumentSection = ({ familyDocuments, userCanModify, onAddNew, @@ -31,7 +30,7 @@ export const DocumentSection: React.FC = ({ updatedDocument, setUpdatedDocument, isNewFamily, -}) => { +}: DocumentSectionProps) => { return ( <> diff --git a/src/components/forms/sections/EventSection.tsx b/src/components/forms/sections/EventSection.tsx index da29d4a..9925981 100644 --- a/src/components/forms/sections/EventSection.tsx +++ b/src/components/forms/sections/EventSection.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Box, Button, @@ -22,7 +21,7 @@ interface EventSectionProps { isNewFamily: boolean } -export const EventSection: React.FC = ({ +export const EventSection = ({ familyEvents, userCanModify, onAddNew, @@ -31,7 +30,7 @@ export const EventSection: React.FC = ({ updatedEvent, setUpdatedEvent, isNewFamily, -}) => { +}: EventSectionProps) => { return ( <> From 5c3c0cdab41f8437aa9df8ee80d024104446facd Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:15:29 +0000 Subject: [PATCH 47/88] Type errors --- src/components/drawers/DocumentEditDrawer.tsx | 4 ++-- src/components/drawers/EntityEditDrawer.tsx | 4 ++-- src/components/drawers/EventEditDrawer.tsx | 4 ++-- src/components/family/ReadOnlyFields.tsx | 4 ++-- src/components/forms/DynamicMetadataFields.tsx | 9 ++++----- src/components/forms/EventForm.tsx | 10 ++-------- src/components/forms/fields/RadioGroupField.tsx | 4 ++-- src/components/forms/fields/SelectField.tsx | 13 +++++++++---- src/components/forms/fields/TextField.tsx | 8 ++++---- src/components/forms/fields/WYSIWYGField.tsx | 4 ++-- src/components/forms/modals/UnsavedChangesModal.tsx | 8 ++------ src/components/forms/sections/DocumentSection.tsx | 4 ++-- src/components/forms/sections/EventSection.tsx | 4 ++-- 13 files changed, 37 insertions(+), 43 deletions(-) diff --git a/src/components/drawers/DocumentEditDrawer.tsx b/src/components/drawers/DocumentEditDrawer.tsx index 7875b16..19ba910 100644 --- a/src/components/drawers/DocumentEditDrawer.tsx +++ b/src/components/drawers/DocumentEditDrawer.tsx @@ -8,7 +8,7 @@ import { import { IDocument, TTaxonomy } from '@/interfaces' import { DocumentForm } from '../forms/DocumentForm' -interface DocumentEditDrawerProps { +type TProps = { document?: IDocument familyId?: string onClose: () => void @@ -26,7 +26,7 @@ export const DocumentEditDrawer = ({ onSuccess, canModify, taxonomy, -}: DocumentEditDrawerProps) => { +}: TProps) => { return ( diff --git a/src/components/drawers/EntityEditDrawer.tsx b/src/components/drawers/EntityEditDrawer.tsx index e7839e2..bc74b71 100644 --- a/src/components/drawers/EntityEditDrawer.tsx +++ b/src/components/drawers/EntityEditDrawer.tsx @@ -2,7 +2,7 @@ import { DocumentEditDrawer } from './DocumentEditDrawer' import { EventEditDrawer } from './EventEditDrawer' import { IDocument, IEvent, TTaxonomy } from '@/interfaces' -interface EntityEditDrawerProps { +type TProps = { isOpen: boolean onClose: () => void entity: 'document' | 'event' @@ -26,7 +26,7 @@ export const EntityEditDrawer = ({ familyId, taxonomy, canModify, -}: EntityEditDrawerProps) => { +}: TProps) => { if (entity === 'document') { return ( void @@ -27,7 +27,7 @@ export const EventEditDrawer = ({ onSuccess, canModify, taxonomy, -}: EventEditDrawerProps) => { +}: TProps) => { return ( diff --git a/src/components/family/ReadOnlyFields.tsx b/src/components/family/ReadOnlyFields.tsx index 932a8b8..27eb21f 100644 --- a/src/components/family/ReadOnlyFields.tsx +++ b/src/components/family/ReadOnlyFields.tsx @@ -1,11 +1,11 @@ import { Box, Text, VStack, Divider } from '@chakra-ui/react' import { TFamily } from '@/interfaces' -interface ReadOnlyFieldsProps { +type TProps = { family: TFamily } -export const ReadOnlyFields = ({ family }: ReadOnlyFieldsProps) => { +export const ReadOnlyFields = ({ family }: TProps) => { return ( <> diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index eef62c0..3151e0c 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -4,14 +4,13 @@ import { FormErrorMessage, FormHelperText, } from '@chakra-ui/react' -import { Control, FieldErrors } from 'react-hook-form' +import { Control, FieldErrors, FieldValues } from 'react-hook-form' import { FieldType } from '@/interfaces/Metadata' import { formatFieldLabel } from '@/utils/metadataUtils' import { SelectField } from './fields/SelectField' import { TextField } from './fields/TextField' -// Interface for rendering dynamic metadata fields -export interface DynamicMetadataFieldProps> { +type TProps = { fieldKey: string taxonomyField: { allowed_values?: string[] @@ -23,13 +22,13 @@ export interface DynamicMetadataFieldProps> { fieldType: FieldType } -export const DynamicMetadataFields = >({ +export const DynamicMetadataFields = ({ fieldKey, taxonomyField, control, errors, fieldType, -}: DynamicMetadataFieldProps) => { +}: TProps) => { const { allowed_values = [], allow_any = false, diff --git a/src/components/forms/EventForm.tsx b/src/components/forms/EventForm.tsx index d90471a..af7e146 100644 --- a/src/components/forms/EventForm.tsx +++ b/src/components/forms/EventForm.tsx @@ -6,8 +6,7 @@ import { IEventFormPost, IError, IEventFormPut, - IConfigTaxonomyCCLW, - IConfigTaxonomyUNFCCC, + TTaxonomy, } from '@/interfaces' import { eventSchema } from '@/schemas/eventSchema' import { createEvent, updateEvent } from '@/api/Events' @@ -26,16 +25,11 @@ import { import { formatDateISO } from '@/utils/formatDate' import { ApiError } from '../feedback/ApiError' -// type TaxonomyEventType = -// | { event_type: string[] } -// | { event_type: { allowed_values: string[] } } -// | undefined - type TProps = { familyId?: string canModify?: boolean event?: IEvent - taxonomy?: IConfigTaxonomyCCLW | IConfigTaxonomyUNFCCC // | TaxonomyEventType + taxonomy?: TTaxonomy onSuccess?: (eventId: string) => void } diff --git a/src/components/forms/fields/RadioGroupField.tsx b/src/components/forms/fields/RadioGroupField.tsx index c60765b..e32be70 100644 --- a/src/components/forms/fields/RadioGroupField.tsx +++ b/src/components/forms/fields/RadioGroupField.tsx @@ -15,7 +15,7 @@ import { } from '@chakra-ui/react' import { IChakraSelect } from '@/interfaces' -interface RadioGroupFieldProps { +type TProps = { name: Path label: string control: Control @@ -29,7 +29,7 @@ export const RadioGroupField = ({ control, options, rules, -}: RadioGroupFieldProps) => { +}: TProps) => { return ( > { +type TProps = { name: string label?: string control: Control @@ -15,7 +20,7 @@ interface SelectFieldProps> { isRequired?: boolean } -export const SelectField = >({ +export const SelectField = ({ name, label, control, @@ -23,7 +28,7 @@ export const SelectField = >({ isMulti = false, rules, isRequired, -}: SelectFieldProps) => { +}: TProps) => { // Determine if options are already in IChakraSelect format const selectOptions = options ? Array.isArray(options) && diff --git a/src/components/forms/fields/TextField.tsx b/src/components/forms/fields/TextField.tsx index 6949a9f..590654d 100644 --- a/src/components/forms/fields/TextField.tsx +++ b/src/components/forms/fields/TextField.tsx @@ -1,4 +1,4 @@ -import { Controller, Control } from 'react-hook-form' +import { Controller, Control, FieldValues } from 'react-hook-form' import { Input, FormControl, @@ -6,7 +6,7 @@ import { FormErrorMessage, } from '@chakra-ui/react' -interface TextFieldProps> { +type TProps = { name: string control: Control type?: 'text' | 'number' @@ -15,14 +15,14 @@ interface TextFieldProps> { isRequired?: boolean } -export const TextField = >({ +export const TextField = ({ name, control, type = 'text', placeholder, label, isRequired, -}: TextFieldProps) => { +}: TProps) => { return ( { +type TProps = { name: Path label: string control: Control @@ -21,7 +21,7 @@ export const WYSIWYGField = ({ onChange, error, isRequired = false, -}: WYSIWYGFieldProps) => { +}: TProps) => { return ( void onConfirm: () => void } -export const UnsavedChangesModal = ({ - isOpen, - onClose, - onConfirm, -}: UnsavedChangesModalProps) => { +export const UnsavedChangesModal = ({ isOpen, onClose, onConfirm }: TProps) => { return ( diff --git a/src/components/forms/sections/DocumentSection.tsx b/src/components/forms/sections/DocumentSection.tsx index d4ada2c..570da32 100644 --- a/src/components/forms/sections/DocumentSection.tsx +++ b/src/components/forms/sections/DocumentSection.tsx @@ -10,7 +10,7 @@ import { WarningIcon } from '@chakra-ui/icons' import { FamilyDocument } from '@/components/family/FamilyDocument' import { IDocument } from '@/interfaces' -interface DocumentSectionProps { +type TProps = { familyDocuments: string[] userCanModify: boolean onAddNew: (type: 'document') => void @@ -30,7 +30,7 @@ export const DocumentSection = ({ updatedDocument, setUpdatedDocument, isNewFamily, -}: DocumentSectionProps) => { +}: TProps) => { return ( <> diff --git a/src/components/forms/sections/EventSection.tsx b/src/components/forms/sections/EventSection.tsx index 9925981..8a2dffe 100644 --- a/src/components/forms/sections/EventSection.tsx +++ b/src/components/forms/sections/EventSection.tsx @@ -10,7 +10,7 @@ import { WarningIcon } from '@chakra-ui/icons' import { FamilyEvent } from '@/components/family/FamilyEvent' import { IEvent } from '@/interfaces' -interface EventSectionProps { +type TProps = { familyEvents: string[] userCanModify: boolean onAddNew: (type: 'event') => void @@ -30,7 +30,7 @@ export const EventSection = ({ updatedEvent, setUpdatedEvent, isNewFamily, -}: EventSectionProps) => { +}: TProps) => { return ( <> From 64975e0eef693ff79a587186e62983d0b325aaaa Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:27:53 +0000 Subject: [PATCH 48/88] Type errors --- src/components/forms/metadata-handlers/familyForm.ts | 8 ++++---- src/components/forms/sections/MetadataSection.tsx | 10 +++++++--- src/interfaces/Family.ts | 2 +- src/schemas/familySchema.ts | 7 +++++-- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/components/forms/metadata-handlers/familyForm.ts b/src/components/forms/metadata-handlers/familyForm.ts index 5567dc0..ecc17aa 100644 --- a/src/components/forms/metadata-handlers/familyForm.ts +++ b/src/components/forms/metadata-handlers/familyForm.ts @@ -8,7 +8,7 @@ import { IInternationalAgreementsMetadata, ILawsAndPoliciesMetadata, TFamilyFormPost, - TFamilyFormPostMetadata, + TFamilyMetadata, } from '../../../interfaces/Family' import { IInternationalAgreementsFamilyFormPost, @@ -16,7 +16,7 @@ import { } from '../../../interfaces/Family' // Type-safe metadata handler type -export type MetadataHandler = { +export type MetadataHandler = { extractMetadata: (formData: TFamilyFormSubmit) => T createSubmissionData: ( baseData: IFamilyFormPostBase, @@ -27,7 +27,7 @@ export type MetadataHandler = { // Mapping of corpus types to their specific metadata handlers export const corpusMetadataHandlers: Record< string, - MetadataHandler + MetadataHandler > = { 'Intl. agreements': { extractMetadata: (formData: TFamilyFormSubmit) => { @@ -70,7 +70,7 @@ export const corpusMetadataHandlers: Record< // Utility function to get metadata handler for a specific corpus type export const getMetadataHandler = ( corpusType: string, -): MetadataHandler => { +): MetadataHandler => { const handler = corpusMetadataHandlers[corpusType] if (!handler) { throw new Error(`Unsupported corpus type: ${corpusType}`) diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 38cd38d..e3c0cd0 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -2,7 +2,11 @@ import { useEffect } from 'react' import { useForm } from 'react-hook-form' import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' import { DynamicMetadataFields } from '../DynamicMetadataFields' -import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' +import { + CORPUS_METADATA_CONFIG, + FieldType, + IMetadata, +} from '@/interfaces/Metadata' import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' type TProps = { @@ -25,8 +29,8 @@ export const MetadataSection = ({ useEffect(() => { if (loadedFamily?.metadata && corpusInfo) { const metadataValues = Object.entries( - loadedFamily.metadata as Record, - ).reduce>((acc, [fieldKey, value]) => { + loadedFamily.metadata as IMetadata, + ).reduce((acc, [fieldKey, value]) => { const fieldConfig = CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[ fieldKey diff --git a/src/interfaces/Family.ts b/src/interfaces/Family.ts index c875a72..657bdf5 100644 --- a/src/interfaces/Family.ts +++ b/src/interfaces/Family.ts @@ -66,7 +66,7 @@ export interface IInternationalAgreementsFamilyFormPost metadata: IInternationalAgreementsMetadata } -export type TFamilyFormPostMetadata = +export type TFamilyMetadata = | IInternationalAgreementsMetadata | ILawsAndPoliciesMetadata diff --git a/src/schemas/familySchema.ts b/src/schemas/familySchema.ts index 353caba..e26382d 100644 --- a/src/schemas/familySchema.ts +++ b/src/schemas/familySchema.ts @@ -1,4 +1,5 @@ import * as yup from 'yup' +import { TFamilyMetadata } from '@/interfaces/Family' // Base schema for core family fields (non-metadata) export const baseFamilySchema = yup @@ -15,12 +16,14 @@ export const baseFamilySchema = yup corpus: yup.object({ label: yup.string().required(), value: yup.string().required(), - }), + }).required('Corpus is required'), collections: yup.array().optional(), }) .required() // Function to merge base schema with dynamic metadata schema -export const createFamilySchema = (metadataSchema: yup.ObjectSchema) => { +export const createFamilySchema = ( + metadataSchema: yup.ObjectSchema, +) => { return baseFamilySchema.concat(metadataSchema) } From 4811e794ce694d0c970d320bed08e076194671dd Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:32:11 +0000 Subject: [PATCH 49/88] Fix tests --- src/tests/utils/modifyConfig.test.ts | 12 ++++++++++++ src/tests/utilsTest/mocks.tsx | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/tests/utils/modifyConfig.test.ts b/src/tests/utils/modifyConfig.test.ts index dc630d2..6e40e4e 100644 --- a/src/tests/utils/modifyConfig.test.ts +++ b/src/tests/utils/modifyConfig.test.ts @@ -77,6 +77,12 @@ describe('modifyConfig', () => { allowed_values: ['type1', 'type2'], }, }, + _event: { + event_type: { + allow_blanks: false, + allowed_values: ['eventType1', 'eventType2'], + }, + }, }, }, { @@ -112,6 +118,12 @@ describe('modifyConfig', () => { allowed_values: ['type1', 'type2'], }, }, + _event: { + event_type: { + allow_blanks: false, + allowed_values: ['eventType1', 'eventType2'], + }, + }, }, }, ], diff --git a/src/tests/utilsTest/mocks.tsx b/src/tests/utilsTest/mocks.tsx index 390e8ec..e0e682f 100644 --- a/src/tests/utilsTest/mocks.tsx +++ b/src/tests/utilsTest/mocks.tsx @@ -261,6 +261,13 @@ const mockCCLWConfig: IConfig = { allowed_values: ['Type One', 'Type Two'], }, }, + _event: { + event_type: { + allow_any: false, + allow_blanks: false, + allowed_values: ['Event One', 'Event Two'], + }, + }, }, }, ], @@ -307,6 +314,13 @@ const mockUNFCCCConfig: IConfig = { allowed_values: ['Type One', 'Type Two'], }, }, + _event: { + event_type: { + allow_any: false, + allow_blanks: false, + allowed_values: ['Event One', 'Event Two'], + }, + }, }, }, ], From 71b07bd140a7e144c62ddb59cc45616109ab5558 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:57:03 +0000 Subject: [PATCH 50/88] Tsc errors --- src/components/drawers/EntityEditDrawer.tsx | 4 ++-- src/components/forms/DocumentForm.tsx | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/drawers/EntityEditDrawer.tsx b/src/components/drawers/EntityEditDrawer.tsx index bc74b71..da5ea13 100644 --- a/src/components/drawers/EntityEditDrawer.tsx +++ b/src/components/drawers/EntityEditDrawer.tsx @@ -8,8 +8,8 @@ type TProps = { entity: 'document' | 'event' document?: IDocument event?: IEvent - onDocumentSuccess?: (document: IDocument) => void - onEventSuccess?: (event: IEvent) => void + onDocumentSuccess?: (documentId: string) => void + onEventSuccess?: (eventId: string) => void familyId?: string taxonomy?: TTaxonomy canModify?: boolean diff --git a/src/components/forms/DocumentForm.tsx b/src/components/forms/DocumentForm.tsx index 64520a1..3040eca 100644 --- a/src/components/forms/DocumentForm.tsx +++ b/src/components/forms/DocumentForm.tsx @@ -229,7 +229,6 @@ export const DocumentForm = ({ Role - - {taxonomy?._document?.type?.allowed_values.map((option) => ( - - ))} + {(taxonomy?._document?.type?.allowed_values ?? []).map( + (option) => ( + + ), + )} Please select a type @@ -273,14 +273,13 @@ export const DocumentForm = ({ Variant - Please select a type + Please select a variant ) }} From 203b10b8b879dd763acef6191a710f1050e4b8c1 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:20:08 +0000 Subject: [PATCH 51/88] Tsc errors --- src/components/forms/sections/MetadataSection.tsx | 13 +++++++------ src/interfaces/Metadata.ts | 5 ++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index e3c0cd0..a9aab25 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -5,6 +5,7 @@ import { DynamicMetadataFields } from '../DynamicMetadataFields' import { CORPUS_METADATA_CONFIG, FieldType, + IFormMetadata, IMetadata, } from '@/interfaces/Metadata' import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' @@ -30,30 +31,30 @@ export const MetadataSection = ({ if (loadedFamily?.metadata && corpusInfo) { const metadataValues = Object.entries( loadedFamily.metadata as IMetadata, - ).reduce((acc, [fieldKey, value]) => { + ).reduce((transformedMetadata, [fieldKey, value]) => { const fieldConfig = CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[ fieldKey ] - if (!fieldConfig) return acc + if (!fieldConfig || !value) return transformedMetadata if (fieldConfig.type === FieldType.SINGLE_SELECT) { - acc[fieldKey] = value?.[0] + transformedMetadata[fieldKey] = value?.[0] ? { value: value[0], label: value[0], } : undefined } else if (fieldConfig.type === FieldType.MULTI_SELECT) { - acc[fieldKey] = value?.map((v: string) => ({ + transformedMetadata[fieldKey] = value?.map((v: string) => ({ value: v, label: v, })) } else { - acc[fieldKey] = value + transformedMetadata[fieldKey] = value } - return acc + return transformedMetadata }, {}) reset(metadataValues) diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index 9ee0d5b..f195934 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -9,6 +9,9 @@ export enum FieldType { export interface IMetadata { [key: string]: string[] } +export interface IFormMetadata { + [key: string]: { value: string; label: string }[] | { value: string; label: string } | string | undefined +} export interface MetadataFieldConfig { type: FieldType @@ -60,4 +63,4 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { renderFields: {}, validationFields: [], }, -} +} \ No newline at end of file From bf6b1a0ad3b0bac37aae87f9bcd372f75c278fea Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:49:38 +0000 Subject: [PATCH 52/88] Tsc errors --- src/interfaces/Metadata.ts | 4 +- src/schemas/dynamicValidationSchema.ts | 63 +++++++++++++------------- src/schemas/familySchema.ts | 10 ++-- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index f195934..d70c60f 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -1,3 +1,5 @@ +import { IChakraSelect } from '.' + export enum FieldType { TEXT = 'text', MULTI_SELECT = 'multi_select', @@ -10,7 +12,7 @@ export interface IMetadata { [key: string]: string[] } export interface IFormMetadata { - [key: string]: { value: string; label: string }[] | { value: string; label: string } | string | undefined + [key: string]: IChakraSelect[] | IChakraSelect | string | number | undefined } export interface MetadataFieldConfig { diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 6f4388b..7c661dc 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -4,52 +4,48 @@ import { CorpusInfo, CORPUS_METADATA_CONFIG, MetadataFieldConfig, + IFormMetadata, } from '@/interfaces/Metadata' import { IChakraSelect, TTaxonomy } from '@/interfaces' import { ITaxonomyField } from '@/interfaces/Config' -// Strongly typed validation schema creator -type ValidationSchema = yup.Schema - // Type-safe field validation function -const getFieldValidation = >( +const getFieldValidation = ( fieldConfig: MetadataFieldConfig, fieldKey: string, isRequired: boolean, taxonomyField?: ITaxonomyField, -): ValidationSchema => { - let fieldValidation: ValidationSchema +): yup.Schema => { + let fieldValidation: yup.Schema switch (fieldConfig.type) { case FieldType.MULTI_SELECT: fieldValidation = yup.array().of( - yup.object({ - value: yup.string(), - label: yup.string(), + yup.object({ + value: yup.string().required(), + label: yup.string().required(), }), - ) as ValidationSchema + ) break case FieldType.SINGLE_SELECT: - fieldValidation = yup.string() as ValidationSchema + fieldValidation = yup.object({ + value: yup.string().required(), + label: yup.string().required(), + }) break case FieldType.TEXT: - fieldValidation = yup.string() as ValidationSchema + fieldValidation = yup.string() break case FieldType.NUMBER: - fieldValidation = yup.number() as ValidationSchema - break - case FieldType.DATE: - fieldValidation = yup.date() as ValidationSchema + fieldValidation = yup.number() break default: - fieldValidation = yup.mixed() as ValidationSchema + fieldValidation = yup.mixed() } // Add required validation if needed if (isRequired) { - fieldValidation = fieldValidation.required( - `${fieldKey} is required`, - ) as ValidationSchema + fieldValidation = fieldValidation.required(`${fieldKey} is required`) } // Add allowed values validation if specified in taxonomy @@ -66,12 +62,15 @@ const getFieldValidation = >( }, ) } else { - // Convert allowed_values to a type that Yup's oneOf can accept - const allowedValues = (taxonomyField.allowed_values || []) as T[keyof T][] - - fieldValidation = fieldValidation.oneOf( - allowedValues, + const allowedValues = taxonomyField.allowed_values || [] + fieldValidation = fieldValidation.test( + 'allowed-values', `${fieldKey} must be one of the allowed values`, + (value: IChakraSelect | string) => { + if (!value) return true + const valueToCheck = typeof value === 'string' ? value : value.value + return allowedValues.includes(valueToCheck) + }, ) } } @@ -80,13 +79,13 @@ const getFieldValidation = >( } // Strongly typed dynamic validation schema generator -export const generateDynamicValidationSchema = ( +export const generateDynamicValidationSchema = ( taxonomy?: TTaxonomy, corpusInfo?: CorpusInfo, -): yup.ObjectSchema => { +): yup.ObjectSchema> => { // Early return if no taxonomy or corpus info if (!taxonomy || !corpusInfo) { - return yup.object({}).required() as yup.ObjectSchema + return yup.object({}).required() as yup.ObjectSchema> } // Get metadata fields and validation fields for the specific corpus type @@ -108,7 +107,7 @@ export const generateDynamicValidationSchema = ( (!taxonomyField || taxonomyField.allow_blanks === false) // Generate field validation - const fieldValidation = getFieldValidation( + const fieldValidation = getFieldValidation( fieldConfig, fieldKey, isRequired, @@ -120,9 +119,11 @@ export const generateDynamicValidationSchema = ( [fieldKey]: fieldValidation, } }, - {} as Partial, + {} as Partial, ) // Create and return the final schema - return yup.object(schemaShape).required() as yup.ObjectSchema + return yup.object(schemaShape).required() as yup.ObjectSchema< + Partial + > } diff --git a/src/schemas/familySchema.ts b/src/schemas/familySchema.ts index e26382d..2a14893 100644 --- a/src/schemas/familySchema.ts +++ b/src/schemas/familySchema.ts @@ -13,10 +13,12 @@ export const baseFamilySchema = yup }) .required('Geography is required'), category: yup.string().required('Category is required'), - corpus: yup.object({ - label: yup.string().required(), - value: yup.string().required(), - }).required('Corpus is required'), + corpus: yup + .object({ + label: yup.string().required(), + value: yup.string().required(), + }) + .required('Corpus is required'), collections: yup.array().optional(), }) .required() From 08ecfdf9ff4aa1bb6f71dff971b0bb1b7ed7bd50 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:45:34 +0000 Subject: [PATCH 53/88] Fix metadata form --- .../forms/sections/MetadataSection.tsx | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index a9aab25..29892ff 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -1,67 +1,63 @@ -import { useEffect } from 'react' -import { useForm } from 'react-hook-form' +import React, { useEffect } from 'react' +import { Control, FieldErrors, UseFormReset } from 'react-hook-form' import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' import { DynamicMetadataFields } from '../DynamicMetadataFields' -import { - CORPUS_METADATA_CONFIG, - FieldType, - IFormMetadata, - IMetadata, -} from '@/interfaces/Metadata' +import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' -type TProps = { +type TProps> = { corpusInfo?: IConfigCorpora taxonomy?: TTaxonomy + control: Control + errors: FieldErrors loadedFamily?: TFamily + reset: UseFormReset } export const MetadataSection = ({ corpusInfo, taxonomy, + control, + errors, loadedFamily, -}: TProps) => { - const { - control, - reset, - formState: { errors }, - } = useForm() - + reset, +}: TProps) => { useEffect(() => { if (loadedFamily?.metadata && corpusInfo) { - const metadataValues = Object.entries( - loadedFamily.metadata as IMetadata, - ).reduce((transformedMetadata, [fieldKey, value]) => { + const metadataValues = Object.entries(loadedFamily.metadata).reduce< + Record + >((acc, [key, value]) => { const fieldConfig = - CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[ - fieldKey - ] - if (!fieldConfig || !value) return transformedMetadata + CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] + if (!fieldConfig) return acc if (fieldConfig.type === FieldType.SINGLE_SELECT) { - transformedMetadata[fieldKey] = value?.[0] + acc[key] = value?.[0] ? { value: value[0], label: value[0], } : undefined } else if (fieldConfig.type === FieldType.MULTI_SELECT) { - transformedMetadata[fieldKey] = value?.map((v: string) => ({ + acc[key] = value?.map((v) => ({ value: v, label: v, })) } else { - transformedMetadata[fieldKey] = value + acc[key] = value } - return transformedMetadata + return acc }, {}) - reset(metadataValues) + reset((formValues) => ({ + ...formValues, + ...metadataValues, + })) } }, [loadedFamily, corpusInfo, reset]) - if (!corpusInfo || !taxonomy) return <> + if (!corpusInfo || !taxonomy) return null return ( <> From 7718a5eebaa16c7265b08bcc1b8ca7e2f88b55d1 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:20:37 +0000 Subject: [PATCH 54/88] Tsc errors --- .../forms/DynamicMetadataFields.tsx | 7 +-- src/components/forms/FamilyForm.tsx | 54 +++++++++++++------ .../forms/metadata-handlers/familyForm.ts | 2 +- src/hooks/useCorpusFromConfig.ts | 2 +- src/schemas/dynamicValidationSchema.ts | 27 ---------- 5 files changed, 42 insertions(+), 50 deletions(-) diff --git a/src/components/forms/DynamicMetadataFields.tsx b/src/components/forms/DynamicMetadataFields.tsx index 3151e0c..37a76ec 100644 --- a/src/components/forms/DynamicMetadataFields.tsx +++ b/src/components/forms/DynamicMetadataFields.tsx @@ -9,14 +9,11 @@ import { FieldType } from '@/interfaces/Metadata' import { formatFieldLabel } from '@/utils/metadataUtils' import { SelectField } from './fields/SelectField' import { TextField } from './fields/TextField' +import { ITaxonomyField } from '@/interfaces' type TProps = { fieldKey: string - taxonomyField: { - allowed_values?: string[] - allow_any?: boolean - allow_blanks?: boolean - } + taxonomyField: ITaxonomyField control: Control errors: FieldErrors fieldType: FieldType diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index ebd40c6..8ee0b1c 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -55,7 +55,7 @@ interface IFamilyFormBase { export interface IFamilyFormIntlAgreements extends IFamilyFormBase { // Intl. agreements author?: string - author_type?: string + author_type?: IChakraSelect } export interface IFamilyFormLawsAndPolicies extends IFamilyFormBase { @@ -104,7 +104,9 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { getCorpusImportId(loadedFamily), getCorpusImportId(loadedFamily), ) - const initialTaxonomy = initialCorpusInfo ? initialCorpusInfo?.taxonomy : null + const initialTaxonomy = initialCorpusInfo + ? initialCorpusInfo?.taxonomy + : undefined // Create validation schema const createValidationSchema = useCallback( @@ -131,12 +133,14 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { setValue, watch, formState: { errors, isSubmitting, dirtyFields }, - } = useForm({ + } = useForm({ resolver: yupResolver(validationSchema), }) // Watch for corpus changes and update schema only when creating a new family - const watchCorpus = !loadedFamily ? watch('corpus') : undefined + const watchCorpus = !loadedFamily + ? watch('corpus') + : loadedFamily.corpus_import_id const corpusInfo = useCorpusFromConfig( config?.corpora, getCorpusImportId(loadedFamily), @@ -201,6 +205,8 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { // Get the appropriate metadata handler & extract metadata const metadataHandler = getMetadataHandler(corpusInfo.corpus_type) const metadata = metadataHandler.extractMetadata(formData) + console.log('Form Data:', formData) + console.log('Extracted Metadata:', metadata) // Create submission data using the specific handler const submissionData = metadataHandler.createSubmissionData( @@ -236,20 +242,39 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { } const onSubmit: SubmitHandler = async (data) => { + console.log('Form Data Before Submission:', data) try { await handleFormSubmission(data) } catch (error) { console.log('onSubmitErrorHandler', error) - // Handle any submission errors - setFormError(error as IError) - toast({ - title: 'Submission Error', - description: (error as IError).message, - status: 'error', - }) } } + // object type is workaround for SubmitErrorHandler throwing a tsc error. + const onSubmitErrorHandler = (error: object) => { + console.log('onSubmitErrorHandler', error) + + // Handle any submission errors + setFormError(error as IError) + toast({ + title: 'Submission Error', + description: (error as IError).message, + status: 'error', + }) + + const submitHandlerErrors = error as { + [key: string]: { message: string; type: string } + } + // Set form errors manually + Object.keys(submitHandlerErrors).forEach((key) => { + if (key === 'summary') + setError('summary', { + type: 'required', + message: 'Summary is required', + }) + }) + } + useEffect(() => { if (loadedFamily) { setFamilyDocuments(loadedFamily.documents || []) @@ -378,8 +403,6 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { const canLoadForm = !configLoading && !collectionsLoading && !configError && !collectionsError - // }, [loadedFamily, collections, reset, isMCFCorpus]) - const blocker = useBlocker( ({ currentLocation, nextLocation }) => !isFormSubmitting && @@ -415,7 +438,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { )} {canLoadForm && ( - + {formError && } @@ -457,7 +480,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { label='Geography' control={control} options={getCountries(config?.geographies).map((country) => ({ - value: country.id, + value: country.value, label: country.display_value, }))} isMulti={false} @@ -490,7 +513,6 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { [ { value: 'Executive', label: 'Executive' }, { value: 'Legislative', label: 'Legislative' }, - { value: 'Litigation', label: 'Litigation' }, { value: 'UNFCCC', label: 'UNFCCC' }, ] } diff --git a/src/components/forms/metadata-handlers/familyForm.ts b/src/components/forms/metadata-handlers/familyForm.ts index ecc17aa..8921619 100644 --- a/src/components/forms/metadata-handlers/familyForm.ts +++ b/src/components/forms/metadata-handlers/familyForm.ts @@ -34,7 +34,7 @@ export const corpusMetadataHandlers: Record< const intlData = formData as IFamilyFormIntlAgreements return { author: intlData.author ? [intlData.author] : [], - author_type: intlData.author_type ? [intlData.author_type] : [], + author_type: intlData.author_type ? [intlData.author_type?.value] : [], } as IInternationalAgreementsMetadata }, createSubmissionData: (baseData, metadata) => diff --git a/src/hooks/useCorpusFromConfig.ts b/src/hooks/useCorpusFromConfig.ts index 213cec4..4542872 100644 --- a/src/hooks/useCorpusFromConfig.ts +++ b/src/hooks/useCorpusFromConfig.ts @@ -11,7 +11,7 @@ const useCorpusFromConfig = ( const corp = corpora?.find( (corpus) => corpus.corpus_import_id === corpusId, ) - return corp ? corp : null + return corp ? corp : undefined } return getCorpusFromId(corpus_id ? corpus_id : watchCorpusValue) diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 7c661dc..1cac03b 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -48,33 +48,6 @@ const getFieldValidation = ( fieldValidation = fieldValidation.required(`${fieldKey} is required`) } - // Add allowed values validation if specified in taxonomy - if (taxonomyField?.allowed_values && !taxonomyField.allow_any) { - if (fieldConfig.type === FieldType.MULTI_SELECT) { - fieldValidation = fieldValidation.test( - 'allowed-values', - `${fieldKey} contains invalid values`, - (value: IChakraSelect[]) => { - if (!value) return true - return value.every((item) => - taxonomyField.allowed_values?.includes(item.value), - ) - }, - ) - } else { - const allowedValues = taxonomyField.allowed_values || [] - fieldValidation = fieldValidation.test( - 'allowed-values', - `${fieldKey} must be one of the allowed values`, - (value: IChakraSelect | string) => { - if (!value) return true - const valueToCheck = typeof value === 'string' ? value : value.value - return allowedValues.includes(valueToCheck) - }, - ) - } - } - return fieldValidation } From d5a1735116a75272b4cc1dd18f74af5c33c75418 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:47:18 +0000 Subject: [PATCH 55/88] Type errors --- src/components/forms/FamilyForm.tsx | 22 ++-------------------- src/interfaces/Metadata.ts | 2 +- src/schemas/dynamicValidationSchema.ts | 15 +++++---------- 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 8ee0b1c..75ff00b 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -52,25 +52,7 @@ interface IFamilyFormBase { collections?: IChakraSelect[] } -export interface IFamilyFormIntlAgreements extends IFamilyFormBase { - // Intl. agreements - author?: string - author_type?: IChakraSelect -} - -export interface IFamilyFormLawsAndPolicies extends IFamilyFormBase { - // Laws and Policies - topic?: IChakraSelect[] - hazard?: IChakraSelect[] - sector?: IChakraSelect[] - keyword?: IChakraSelect[] - framework?: IChakraSelect[] - instrument?: IChakraSelect[] -} - -export type TFamilyFormSubmit = - | IFamilyFormIntlAgreements - | IFamilyFormLawsAndPolicies +export type TFamilyFormSubmit = IFamilyFormBase type TChildEntity = 'event' | 'document' @@ -111,7 +93,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { // Create validation schema const createValidationSchema = useCallback( (currentTaxonomy?: TTaxonomy, currentCorpusInfo?: IConfigCorpora) => { - const metadataSchema = generateDynamicValidationSchema( + const metadataSchema = generateDynamicValidationSchema( currentTaxonomy, currentCorpusInfo, ) diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index d70c60f..30b6057 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -65,4 +65,4 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { renderFields: {}, validationFields: [], }, -} \ No newline at end of file +} diff --git a/src/schemas/dynamicValidationSchema.ts b/src/schemas/dynamicValidationSchema.ts index 1cac03b..fc8a76e 100644 --- a/src/schemas/dynamicValidationSchema.ts +++ b/src/schemas/dynamicValidationSchema.ts @@ -6,15 +6,13 @@ import { MetadataFieldConfig, IFormMetadata, } from '@/interfaces/Metadata' -import { IChakraSelect, TTaxonomy } from '@/interfaces' -import { ITaxonomyField } from '@/interfaces/Config' +import { TFamilyMetadata, TTaxonomy } from '@/interfaces' // Type-safe field validation function const getFieldValidation = ( fieldConfig: MetadataFieldConfig, fieldKey: string, isRequired: boolean, - taxonomyField?: ITaxonomyField, ): yup.Schema => { let fieldValidation: yup.Schema @@ -55,10 +53,10 @@ const getFieldValidation = ( export const generateDynamicValidationSchema = ( taxonomy?: TTaxonomy, corpusInfo?: CorpusInfo, -): yup.ObjectSchema> => { +): yup.ObjectSchema => { // Early return if no taxonomy or corpus info if (!taxonomy || !corpusInfo) { - return yup.object({}).required() as yup.ObjectSchema> + return yup.object({}).required() as yup.ObjectSchema } // Get metadata fields and validation fields for the specific corpus type @@ -84,7 +82,6 @@ export const generateDynamicValidationSchema = ( fieldConfig, fieldKey, isRequired, - taxonomyField, ) return { @@ -92,11 +89,9 @@ export const generateDynamicValidationSchema = ( [fieldKey]: fieldValidation, } }, - {} as Partial, + {} as TFamilyMetadata, ) // Create and return the final schema - return yup.object(schemaShape).required() as yup.ObjectSchema< - Partial - > + return yup.object(schemaShape).required() as yup.ObjectSchema } From b8e9ee8a186b9690d7b5e7e8cbbb38df8cbe9142 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:53:40 +0000 Subject: [PATCH 56/88] Type errors --- src/components/forms/FamilyForm.tsx | 2 +- .../forms/metadata-handlers/familyForm.ts | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 75ff00b..069dce1 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -43,7 +43,7 @@ import { IError } from '@/interfaces/Auth' import { IChakraSelect, IConfigCorpora, TTaxonomy } from '@/interfaces' import { getMetadataHandler } from './metadata-handlers/familyForm' -interface IFamilyFormBase { +export interface IFamilyFormBase { title: string summary: string geography: IChakraSelect diff --git a/src/components/forms/metadata-handlers/familyForm.ts b/src/components/forms/metadata-handlers/familyForm.ts index 8921619..c420009 100644 --- a/src/components/forms/metadata-handlers/familyForm.ts +++ b/src/components/forms/metadata-handlers/familyForm.ts @@ -1,6 +1,5 @@ import { - IFamilyFormIntlAgreements, - IFamilyFormLawsAndPolicies, + IFamilyFormBase, TFamilyFormSubmit, } from '@/components/forms/FamilyForm' import { @@ -14,6 +13,7 @@ import { IInternationalAgreementsFamilyFormPost, ILawsAndPoliciesFamilyFormPost, } from '../../../interfaces/Family' +import { IChakraSelect } from '@/interfaces' // Type-safe metadata handler type export type MetadataHandler = { @@ -24,6 +24,22 @@ export type MetadataHandler = { ) => TFamilyFormPost } +export interface IFamilyFormIntlAgreements extends IFamilyFormBase { + // Intl. agreements + author?: string + author_type?: IChakraSelect +} + +export interface IFamilyFormLawsAndPolicies extends IFamilyFormBase { + // Laws and Policies + topic?: IChakraSelect[] + hazard?: IChakraSelect[] + sector?: IChakraSelect[] + keyword?: IChakraSelect[] + framework?: IChakraSelect[] + instrument?: IChakraSelect[] +} + // Mapping of corpus types to their specific metadata handlers export const corpusMetadataHandlers: Record< string, From ff468fb389df473a791510af40f331c4e06bdab4 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:04:15 +0000 Subject: [PATCH 57/88] Type errors --- src/components/forms/sections/MetadataSection.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 29892ff..baf97e9 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -1,9 +1,10 @@ -import React, { useEffect } from 'react' +import { useEffect } from 'react' import { Control, FieldErrors, UseFormReset } from 'react-hook-form' import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' import { DynamicMetadataFields } from '../DynamicMetadataFields' import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' +import { IFamilyFormBase } from '../FamilyForm' type TProps> = { corpusInfo?: IConfigCorpora @@ -50,7 +51,7 @@ export const MetadataSection = ({ return acc }, {}) - reset((formValues) => ({ + reset((formValues: IFamilyFormBase) => ({ ...formValues, ...metadataValues, })) From effd2acf64211bf0b77712df80db63e95a5473c2 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:14:38 +0000 Subject: [PATCH 58/88] Type errors --- src/components/forms/sections/MetadataSection.tsx | 10 +++++----- src/interfaces/Config.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index baf97e9..3f0a1e4 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -6,13 +6,13 @@ import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' import { IFamilyFormBase } from '../FamilyForm' -type TProps> = { +type TProps = { corpusInfo?: IConfigCorpora taxonomy?: TTaxonomy - control: Control - errors: FieldErrors + control: Control + errors: FieldErrors loadedFamily?: TFamily - reset: UseFormReset + reset: UseFormReset } export const MetadataSection = ({ @@ -22,7 +22,7 @@ export const MetadataSection = ({ errors, loadedFamily, reset, -}: TProps) => { +}: TProps) => { useEffect(() => { if (loadedFamily?.metadata && corpusInfo) { const metadataValues = Object.entries(loadedFamily.metadata).reduce< diff --git a/src/interfaces/Config.ts b/src/interfaces/Config.ts index ef60780..7444835 100644 --- a/src/interfaces/Config.ts +++ b/src/interfaces/Config.ts @@ -23,7 +23,7 @@ export interface ITaxonomyField { allow_blanks?: boolean } -interface ISubTaxonomy { +export interface ISubTaxonomy { [key: string]: ITaxonomyField } @@ -38,7 +38,7 @@ export interface IEventSubTaxonomy extends ISubTaxonomy { export type TSubTaxonomy = IEventSubTaxonomy | IDocumentSubTaxonomy -export interface ITaxonomy { +interface ITaxonomy { [key: string]: ITaxonomyField | TSubTaxonomy } From 48b43d9d2cbc473171bbffb2b0dc2fdde85d9e86 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:22:22 +0000 Subject: [PATCH 59/88] Type errors --- .../forms/sections/MetadataSection.tsx | 19 ++++++++++++++----- src/interfaces/Metadata.ts | 11 +++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/forms/sections/MetadataSection.tsx b/src/components/forms/sections/MetadataSection.tsx index 3f0a1e4..845e268 100644 --- a/src/components/forms/sections/MetadataSection.tsx +++ b/src/components/forms/sections/MetadataSection.tsx @@ -2,8 +2,17 @@ import { useEffect } from 'react' import { Control, FieldErrors, UseFormReset } from 'react-hook-form' import { Box, Divider, AbsoluteCenter } from '@chakra-ui/react' import { DynamicMetadataFields } from '../DynamicMetadataFields' -import { CORPUS_METADATA_CONFIG, FieldType } from '@/interfaces/Metadata' -import { IConfigCorpora, TFamily, TTaxonomy } from '@/interfaces' +import { + CORPUS_METADATA_CONFIG, + FieldType, + IFormMetadata, +} from '@/interfaces/Metadata' +import { + IConfigCorpora, + TFamily, + TFamilyMetadata, + TTaxonomy, +} from '@/interfaces' import { IFamilyFormBase } from '../FamilyForm' type TProps = { @@ -25,9 +34,9 @@ export const MetadataSection = ({ }: TProps) => { useEffect(() => { if (loadedFamily?.metadata && corpusInfo) { - const metadataValues = Object.entries(loadedFamily.metadata).reduce< - Record - >((acc, [key, value]) => { + const metadataValues = Object.entries( + loadedFamily.metadata as TFamilyMetadata, + ).reduce((acc, [key, value]) => { const fieldConfig = CORPUS_METADATA_CONFIG[corpusInfo.corpus_type]?.renderFields?.[key] if (!fieldConfig) return acc diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index 30b6057..c6e1864 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -11,8 +11,15 @@ export enum FieldType { export interface IMetadata { [key: string]: string[] } + export interface IFormMetadata { - [key: string]: IChakraSelect[] | IChakraSelect | string | number | undefined + [key: string]: + | string + | IChakraSelect[] + | IChakraSelect + | string[] + | number + | undefined } export interface MetadataFieldConfig { @@ -65,4 +72,4 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { renderFields: {}, validationFields: [], }, -} +} \ No newline at end of file From 9ec1cf08ee766f25c3e9101ed56c246c12298934 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:24:07 +0000 Subject: [PATCH 60/88] Type errors --- src/components/forms/FamilyForm.tsx | 4 +--- src/interfaces/Metadata.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index 069dce1..e93fb04 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -120,9 +120,7 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { }) // Watch for corpus changes and update schema only when creating a new family - const watchCorpus = !loadedFamily - ? watch('corpus') - : loadedFamily.corpus_import_id + const watchCorpus = !loadedFamily ? watch('corpus') : undefined const corpusInfo = useCorpusFromConfig( config?.corpora, getCorpusImportId(loadedFamily), diff --git a/src/interfaces/Metadata.ts b/src/interfaces/Metadata.ts index c6e1864..3db862c 100644 --- a/src/interfaces/Metadata.ts +++ b/src/interfaces/Metadata.ts @@ -72,4 +72,4 @@ export const CORPUS_METADATA_CONFIG: CorpusMetadataConfig = { renderFields: {}, validationFields: [], }, -} \ No newline at end of file +} From 9a544ed12e75af38be16be856a6154b0035a4938 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:33:44 +0000 Subject: [PATCH 61/88] Remove unused code --- src/components/forms/FamilyForm.tsx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/forms/FamilyForm.tsx b/src/components/forms/FamilyForm.tsx index e93fb04..64941f2 100644 --- a/src/components/forms/FamilyForm.tsx +++ b/src/components/forms/FamilyForm.tsx @@ -237,22 +237,10 @@ export const FamilyForm = ({ family: loadedFamily }: TProps) => { // Handle any submission errors setFormError(error as IError) toast({ - title: 'Submission Error', + title: 'Form submission error', description: (error as IError).message, status: 'error', }) - - const submitHandlerErrors = error as { - [key: string]: { message: string; type: string } - } - // Set form errors manually - Object.keys(submitHandlerErrors).forEach((key) => { - if (key === 'summary') - setError('summary', { - type: 'required', - message: 'Summary is required', - }) - }) } useEffect(() => { From 6774abfd3ca09a6b0f0ffd844e7817a2011639e1 Mon Sep 17 00:00:00 2001 From: Katy Baulch <46493669+katybaulch@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:39:22 +0000 Subject: [PATCH 62/88] Tsc errors --- src/components/forms/DocumentForm.tsx | 3 +++ src/schemas/documentSchema.ts | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/forms/DocumentForm.tsx b/src/components/forms/DocumentForm.tsx index 3040eca..19c040a 100644 --- a/src/components/forms/DocumentForm.tsx +++ b/src/components/forms/DocumentForm.tsx @@ -229,6 +229,7 @@ export const DocumentForm = ({ Role + {(taxonomy?._document?.type?.allowed_values ?? []).map( (option) => (