diff --git a/app/components/CategoryModal/index.tsx b/app/components/CategoryModal/index.tsx index 31c97e1..34f6034 100644 --- a/app/components/CategoryModal/index.tsx +++ b/app/components/CategoryModal/index.tsx @@ -39,6 +39,7 @@ import useFilterState from '#hooks/useFilterState'; import { errorMessage, idSelector, + transformToFormError, } from '#utils/common'; type ThematicArea = NonNullable['results'][number]>; @@ -146,11 +147,17 @@ function CategoryModal(props: Props) { const actionPending = createPending || updatePending || deletePending; const handleResult = useCallback(( - ok: boolean | undefined, + result: { ok?: boolean | null; errors?: unknown } | null | undefined, successMessage: string, ) => { - if (!ok) { - alert.show(errorMessage, { variant: 'danger' }); + if (!result?.ok) { + const serverErrors = isDefined(result?.errors) + ? transformToFormError(result?.errors as Parameters[0]) + : undefined; + const messages = serverErrors + ? Object.values(serverErrors).filter(isDefined).join(' ') + : undefined; + alert.show(messages || errorMessage, { variant: 'danger' }); return; } setCategoryName(undefined); @@ -166,7 +173,7 @@ function CategoryModal(props: Props) { return; } createThematicArea({ data: { name } }).then((resp) => { - handleResult(resp.data?.createThematicArea?.ok, 'Category added successfully'); + handleResult(resp.data?.createThematicArea, 'Category added successfully'); }).catch(() => { alert.show(errorMessage, { variant: 'danger' }); }); @@ -178,7 +185,7 @@ function CategoryModal(props: Props) { return; } updateThematicArea({ id: editingId, data: { name } }).then((resp) => { - handleResult(resp.data?.updateThematicArea?.ok, 'Category updated successfully'); + handleResult(resp.data?.updateThematicArea, 'Category updated successfully'); }).catch(() => { alert.show(errorMessage, { variant: 'danger' }); }); @@ -196,7 +203,7 @@ function CategoryModal(props: Props) { const handleDelete = useCallback((id: string) => { deleteThematicArea({ id }).then((resp) => { - handleResult(resp.data?.deleteThematicArea?.ok, 'Category deleted successfully'); + handleResult(resp.data?.deleteThematicArea, 'Category deleted successfully'); }).catch(() => { alert.show(errorMessage, { variant: 'danger' }); }); diff --git a/app/components/ConfirmModal/index.tsx b/app/components/ConfirmModal/index.tsx deleted file mode 100644 index 6147a44..0000000 --- a/app/components/ConfirmModal/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { type ReactNode } from 'react'; -import { - Button, - ListView, - Modal, -} from '@ifrc-go/ui'; - -export interface Props { - heading?: ReactNode; - message: ReactNode; - confirmLabel?: string; - cancelLabel?: string; - onConfirm: () => void; - onCancel: () => void; - pending?: boolean; -} - -function ConfirmModal(props: Props) { - const { - heading = 'Confirm', - message, - confirmLabel = 'Okay', - cancelLabel = 'Cancel', - onConfirm, - onCancel, - pending, - } = props; - - return ( - - - - - )} - > - {message} - - ); -} - -export default ConfirmModal; diff --git a/app/components/EditDeleteActions/index.tsx b/app/components/EditDeleteActions/index.tsx index ef4ed24..97fbd66 100644 --- a/app/components/EditDeleteActions/index.tsx +++ b/app/components/EditDeleteActions/index.tsx @@ -3,10 +3,13 @@ import { DeleteBinLineIcon, EditTwoLineIcon, } from '@ifrc-go/icons'; -import { TableActions } from '@ifrc-go/ui'; +import { + Button, + ConfirmButton, + TableActions, +} from '@ifrc-go/ui'; import { isDefined } from '@togglecorp/fujs'; -import DropdownMenuItem from '#components/DropdownMenuItem'; import useRouting, { type RoutesMap } from '#hooks/useRouting'; export interface Props { @@ -41,32 +44,25 @@ function EditDeleteActions(props: Props) { }, [navigate, to, id, member, dashboard]); return ( - - } - onClick={handleEditClick} - persist - > - Edit - - } - confirmMessage={`Are you sure you want to delete ${`"${itemTitle}"` || 'this item'}? This action cannot be undone.`} - persist - > - Delete - - - )} - /> + + + + + + ); } diff --git a/app/components/FileInput/index.tsx b/app/components/FileInput/index.tsx new file mode 100644 index 0000000..c470362 --- /dev/null +++ b/app/components/FileInput/index.tsx @@ -0,0 +1,78 @@ +import { useCallback } from 'react'; +import { + Description, + InlineLayout, + RawFileInput, +} from '@ifrc-go/ui'; +import { isDefined } from '@togglecorp/fujs'; +import { type Error } from '@togglecorp/toggle-form'; + +import NonFieldError from '#components/NonFieldError'; +import useAlert from '#hooks/useAlert'; + +interface Props { + name: N; + fileName?: string; + onChange: (file: File | undefined, name: N) => void; + error?: Error; + maxSize?: number; + accept?: string; + disabled?: boolean; + placeholder?: string; +} + +function FileInput(props: Props) { + const { + name, + fileName, + onChange, + error, + maxSize, + accept, + disabled, + placeholder = 'Please upload a file', + } = props; + + const alert = useAlert(); + + const handleChange = useCallback( + (file: File | undefined, inputName: N) => { + if (isDefined(file) && isDefined(maxSize) && file.size > maxSize) { + alert.show( + `File must be ${Math.round(maxSize / (1024 * 1024))}MB or smaller`, + { variant: 'danger' }, + ); + return; + } + onChange(file, inputName); + }, + [alert, maxSize, onChange], + ); + + return ( + <> + + {isDefined(fileName) ? 'Change file' : 'Upload file'} + + )} + > + + {isDefined(fileName) ? fileName : placeholder} + + + + + ); +} + +export default FileInput; diff --git a/app/views/CapacityAndResources/CapacityAndResourcesFilter/index.tsx b/app/views/CapacityAndResources/CapacityAndResourcesFilter/index.tsx index 5fec80a..cb816f4 100644 --- a/app/views/CapacityAndResources/CapacityAndResourcesFilter/index.tsx +++ b/app/views/CapacityAndResources/CapacityAndResourcesFilter/index.tsx @@ -1,12 +1,9 @@ -import { useState } from 'react'; import { SelectInput, TextInput, } from '@ifrc-go/ui'; import { type EntriesAsList } from '@togglecorp/toggle-form'; -import RegionSearchMultiSelectInput, { type AdminAreaItem } from '#components/RegionSearchMultiSelectInput'; -import { AdminAreaLevel } from '#generated/types/graphql'; import { labelSelector, statusFilterOptions, @@ -21,20 +18,8 @@ export interface Props { } function CapacityAndResourcesFilter({ value, onChange }: Props) { - const [regionOptions, setRegionOptions] = useState< - AdminAreaItem[] | undefined | null - >([]); return ( <> - - - - createSubmitHandler( validate, setError, @@ -166,22 +163,7 @@ function ResourceDashboardForm() { [validate, setError, dashboard, handleUpdate, handleCreate], ); - const handleFormSubmit = useCallback(() => { - if (isDefined(dashboard) && showOnHome && value.isActive === false) { - setConfirmShown(true); - return; - } - runSubmit(); - }, [dashboard, showOnHome, value.isActive, runSubmit]); - - const handleConfirm = useCallback(() => { - setConfirmShown(false); - runSubmit(); - }, [runSubmit]); - - const handleConfirmCancel = useCallback(() => { - setConfirmShown(false); - }, []); + const requiresConfirmation = isDefined(dashboard) && showOnHome && value.isActive === false; const handleCancel = useCallback(() => { if (isDefined(id)) { @@ -232,14 +214,27 @@ function ResourceDashboardForm() { > Cancel - + {requiresConfirmation ? ( + + Save + + ) : ( + + )} )} > @@ -330,15 +325,6 @@ function ResourceDashboardForm() { - {confirmShown && ( - - )} ); } diff --git a/app/views/CapacityAndResources/index.tsx b/app/views/CapacityAndResources/index.tsx index ba7290e..dbce4cf 100644 --- a/app/views/CapacityAndResources/index.tsx +++ b/app/views/CapacityAndResources/index.tsx @@ -19,7 +19,6 @@ import EditDeleteActions, { type Props as EditDeleteActionsProps } from '#compon import Link, { type Props as LinkProps } from '#components/Link'; import StatusCell from '#components/StatusCell'; import { - AdminAreaLevel, type CapacityAndResourceFilter, type CapacityAndResourcesQuery, useCapacityAndResourcesQuery, @@ -27,7 +26,6 @@ import { } from '#generated/types/graphql'; import useAlert from '#hooks/useAlert'; import useFilterState from '#hooks/useFilterState'; -import useRegionMap from '#hooks/useRegionMap'; import useRouting from '#hooks/useRouting'; import { errorMessage, @@ -45,7 +43,6 @@ export interface ResourcesFilterType extends Omit (isDefined(item.dashboardsCount) ? String(item.dashboardsCount) : '-'), ), - createStringColumn( - 'region', - 'Region', - (item) => (isDefined(item.regionId) ? regionMap[item.regionId] : '-'), - ), createElementColumn( 'status', 'Status', @@ -152,7 +141,7 @@ function CapacityAndResourcesList() { }), { columnWidth: 150 }, ), - ], [onDeleteClick, regionMap]); + ], [onDeleteClick]); const handleCreateClick = useCallback(() => { navigate('createResources'); diff --git a/app/views/CapacityAndResources/query.ts b/app/views/CapacityAndResources/query.ts index dc738da..14664e3 100644 --- a/app/views/CapacityAndResources/query.ts +++ b/app/views/CapacityAndResources/query.ts @@ -11,7 +11,6 @@ const CAPACITY_AND_RESOURCES = gql` dashboardsCount isActive order - regionId title updatedAt } @@ -41,7 +40,6 @@ const CAPACITY_AND_RESOURCE_DETAIL = gql` description isActive order - regionId title } } diff --git a/app/views/DataAndReports/DataAndReportsForm/index.tsx b/app/views/DataAndReports/DataAndReportsForm/index.tsx index 37b98d8..a648f75 100644 --- a/app/views/DataAndReports/DataAndReportsForm/index.tsx +++ b/app/views/DataAndReports/DataAndReportsForm/index.tsx @@ -9,10 +9,7 @@ import { Button, Container, DateInput, - Description, Image, - InlineLayout, - InputError, InputSection, ListView, RadioInput, @@ -27,7 +24,6 @@ import { import { createSubmitHandler, getErrorObject, - getErrorString, type ObjectSchema, type PartialForm, removeNull, @@ -36,6 +32,7 @@ import { } from '@togglecorp/toggle-form'; import EmbedPreview from '#components/EmbedPreview'; +import FileInput from '#components/FileInput'; import NonFieldError from '#components/NonFieldError'; import RegionSelectInput from '#components/RegionSelectInput'; import { @@ -197,15 +194,18 @@ function DataAndReportsForm() { } }, [coverImagePreview, value.coverImage]); - const handleFileInputChange = useCallback( - (file: File | undefined, name: 'coverImage' | 'file') => { - if (isDefined(file) && file.size > MAX_FILE_SIZE) { - alert.show('File must be 5MB or smaller', { variant: 'danger' }); - return; - } + const handleCoverImageChange = useCallback( + (file: File | undefined, name: 'coverImage') => { + setFieldValue(file, name); + }, + [setFieldValue], + ); + + const handleFileChange = useCallback( + (file: File | undefined, name: 'file') => { setFieldValue(file, name); }, - [alert, setFieldValue], + [setFieldValue], ); const handleResult = useCallback(( @@ -371,11 +371,11 @@ function DataAndReportsForm() { )} + - - {isDefined(fileName) ? 'Change file' : 'Upload file'} - - )} - > - - {isDefined(fileName) ? fileName : 'Please upload a file'} - - - {isDefined(error?.file) && ( - - {getErrorString(error.file)} - - )} + )} ( 'category', 'Category', - (item) => thematicAreaMap[item.thematicAreaId], + (item) => (isDefined(item.thematicAreaId) ? thematicAreaMap[item.thematicAreaId] : '-'), ), createDateColumn( 'updatedAt', diff --git a/app/views/OurWorks/WorksForm/index.tsx b/app/views/OurWorks/WorksForm/index.tsx index 662e45b..7558420 100644 --- a/app/views/OurWorks/WorksForm/index.tsx +++ b/app/views/OurWorks/WorksForm/index.tsx @@ -1,12 +1,12 @@ import { useCallback, useEffect, - useState, } from 'react'; import { useParams } from 'react-router'; import { BlockLoading, Button, + ConfirmButton, Container, InputSection, ListView, @@ -29,7 +29,6 @@ import { useForm, } from '@togglecorp/toggle-form'; -import ConfirmModal from '#components/ConfirmModal'; import EmbedPreview from '#components/EmbedPreview'; import RegionSelectInput from '#components/RegionSelectInput'; import { @@ -92,8 +91,6 @@ function WorksForm() { const navigate = useRouting(); const alert = useAlert(); - const [confirmShown, setConfirmShown] = useState(false); - const { setFieldValue, error: formError, @@ -156,7 +153,7 @@ function WorksForm() { const showOnHome = data?.externalDashboard?.showOnHome ?? false; - const runSubmit = useCallback( + const handleFormSubmit = useCallback( () => createSubmitHandler( validate, setError, @@ -165,22 +162,7 @@ function WorksForm() { [validate, setError, id, handleUpdate, handleCreate], ); - const handleFormSubmit = useCallback(() => { - if (isDefined(id) && showOnHome && value.isActive === false) { - setConfirmShown(true); - return; - } - runSubmit(); - }, [id, showOnHome, value.isActive, runSubmit]); - - const handleConfirm = useCallback(() => { - setConfirmShown(false); - runSubmit(); - }, [runSubmit]); - - const handleConfirmCancel = useCallback(() => { - setConfirmShown(false); - }, []); + const requiresConfirmation = isDefined(id) && showOnHome && value.isActive === false; const handleCancel = useCallback(() => { navigate('ourWorks'); @@ -224,14 +206,27 @@ function WorksForm() { > Cancel - + {requiresConfirmation ? ( + + Save + + ) : ( + + )} )} > @@ -336,15 +331,6 @@ function WorksForm() { - {confirmShown && ( - - )} ); } diff --git a/app/views/Preparedness/PreparednessForm/index.tsx b/app/views/Preparedness/PreparednessForm/index.tsx index 075eca3..bd7ea6f 100644 --- a/app/views/Preparedness/PreparednessForm/index.tsx +++ b/app/views/Preparedness/PreparednessForm/index.tsx @@ -1,12 +1,12 @@ import { useCallback, useEffect, - useState, } from 'react'; import { useParams } from 'react-router'; import { BlockLoading, Button, + ConfirmButton, Container, InputSection, ListView, @@ -29,7 +29,6 @@ import { useForm, } from '@togglecorp/toggle-form'; -import ConfirmModal from '#components/ConfirmModal'; import EmbedPreview from '#components/EmbedPreview'; import RegionSelectInput from '#components/RegionSelectInput'; import { @@ -92,8 +91,6 @@ function PreparednessForm() { const navigate = useRouting(); const alert = useAlert(); - const [confirmShown, setConfirmShown] = useState(false); - const { setFieldValue, error: formError, @@ -156,7 +153,7 @@ function PreparednessForm() { const showOnHome = data?.externalDashboard?.showOnHome ?? false; - const runSubmit = useCallback( + const handleFormSubmit = useCallback( () => createSubmitHandler( validate, setError, @@ -165,22 +162,7 @@ function PreparednessForm() { [validate, setError, id, handleUpdate, handleCreate], ); - const handleFormSubmit = useCallback(() => { - if (isDefined(id) && showOnHome && value.isActive === false) { - setConfirmShown(true); - return; - } - runSubmit(); - }, [id, showOnHome, value.isActive, runSubmit]); - - const handleConfirm = useCallback(() => { - setConfirmShown(false); - runSubmit(); - }, [runSubmit]); - - const handleConfirmCancel = useCallback(() => { - setConfirmShown(false); - }, []); + const requiresConfirmation = isDefined(id) && showOnHome && value.isActive === false; const handleCancel = useCallback(() => { navigate('preparedness'); @@ -224,14 +206,27 @@ function PreparednessForm() { > Cancel - + {requiresConfirmation ? ( + + Save + + ) : ( + + )} )} > @@ -334,15 +329,6 @@ function PreparednessForm() { - {confirmShown && ( - - )} ); } diff --git a/backend b/backend index 8ae1d8f..cca87cb 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 8ae1d8f6c1be1e6f852e78217d3952cbadbb0374 +Subproject commit cca87cbf1b50ad0436a34efe48e74a3cb17a01b3