From 8b21354facb00ede7524b929503d700791d61664 Mon Sep 17 00:00:00 2001 From: amrit Date: Wed, 24 Jun 2026 20:50:26 +0545 Subject: [PATCH 1/2] feat(data-and-reports): add data and reports module --- app/Root/config/routes.ts | 14 + app/utils/common.ts | 9 + .../DataAndReports/CategoryModal/index.tsx | 270 +++++++++ .../DataAndReportsFilters/index.tsx | 45 ++ .../DataAndReportsForm/index.tsx | 511 ++++++++++++++++++ app/views/DataAndReports/index.tsx | 243 ++++++++- app/views/DataAndReports/query.ts | 168 ++++++ backend | 2 +- 8 files changed, 1258 insertions(+), 4 deletions(-) create mode 100644 app/views/DataAndReports/CategoryModal/index.tsx create mode 100644 app/views/DataAndReports/DataAndReportsFilters/index.tsx create mode 100644 app/views/DataAndReports/DataAndReportsForm/index.tsx create mode 100644 app/views/DataAndReports/query.ts diff --git a/app/Root/config/routes.ts b/app/Root/config/routes.ts index 4f3da72..9849234 100644 --- a/app/Root/config/routes.ts +++ b/app/Root/config/routes.ts @@ -123,6 +123,18 @@ const dataAndReports: RouteConfig = { visibility: 'is-authenticated', }; +const createDataAndReports: RouteConfig = { + path: '/data-and-reports/new', + load: () => import('#views/DataAndReports/DataAndReportsForm'), + visibility: 'is-authenticated', +}; + +const editDataAndReports: RouteConfig = { + path: '/data-and-reports/:id/edit', + load: () => import('#views/DataAndReports/DataAndReportsForm'), + visibility: 'is-authenticated', +}; + const capacityAndResources: RouteConfig = { index: true, path: '/capacity-and-resources', @@ -167,6 +179,8 @@ const routes = { createPreparedness, editPreparedness, dataAndReports, + createDataAndReports, + editDataAndReports, capacityAndResources, documents, onlineInteractive, diff --git a/app/utils/common.ts b/app/utils/common.ts index 463e508..cf683f2 100644 --- a/app/utils/common.ts +++ b/app/utils/common.ts @@ -23,6 +23,15 @@ export function valueSelector(item: { value: T }) { return item.value; } +export function omitKeys( + obj: T, + keys: readonly K[], +): Omit { + const result = { ...obj }; + keys.forEach((key) => { delete result[key]; }); + return result; +} + // Boolean values for RadioInput (used in forms) export const statusOptions = [ { label: 'Active', value: true }, diff --git a/app/views/DataAndReports/CategoryModal/index.tsx b/app/views/DataAndReports/CategoryModal/index.tsx new file mode 100644 index 0000000..d1c6470 --- /dev/null +++ b/app/views/DataAndReports/CategoryModal/index.tsx @@ -0,0 +1,270 @@ +import { + useCallback, + useState, +} from 'react'; +import { + AddLineIcon, + CheckLineIcon, + CloseLineIcon, + DeleteBinLineIcon, + EditTwoLineIcon, +} from '@ifrc-go/icons'; +import { + IconButton, + InlineLayout, + List, + ListView, + Modal, + Pager, + TextInput, +} from '@ifrc-go/ui'; +import { isDefined } from '@togglecorp/fujs'; + +import { + type ThematicAreasQuery, + useCreateThematicAreaMutation, + useDeleteThematicAreaMutation, + useThematicAreasQuery, + useUpdateThematicAreaMutation, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import useFilterState from '#hooks/useFilterState'; +import { + errorMessage, + idSelector, +} from '#utils/common'; + +type ThematicArea = NonNullable['results'][number]>; + +interface CategoryFilterType { + search: string | undefined; +} + +const defaultFilter: CategoryFilterType = { + search: undefined, +}; + +interface CategoryItemProps { + category: ThematicArea; + onEdit: (id: string) => void; + onDelete: (id: string) => void; + disabled: boolean; +} + +function CategoryItem(props: CategoryItemProps) { + const { + category, + onEdit, + onDelete, + disabled, + } = props; + + return ( + + {category.name} + + + + + + + + + + ); +} + +export interface Props { + onClose: () => void; + onCategoriesChange: () => void; +} + +function CategoryModal(props: Props) { + const { + onClose, + onCategoriesChange, + } = props; + + const alert = useAlert(); + + const { + rawFilter, + filter, + filtered, + setFilterField, + page, + setPage, + limit, + offset, + } = useFilterState({ + filter: defaultFilter, + }); + + const [categoryName, setCategoryName] = useState(); + const [editingId, setEditingId] = useState(); + + const [{ fetching, data, error }, reExecuteQuery] = useThematicAreasQuery({ + variables: { + pagination: { limit, offset }, + filters: { search: filter.search || undefined }, + }, + }); + const [{ fetching: createPending }, createThematicArea] = useCreateThematicAreaMutation(); + const [{ fetching: updatePending }, updateThematicArea] = useUpdateThematicAreaMutation(); + const [{ fetching: deletePending }, deleteThematicArea] = useDeleteThematicAreaMutation(); + + const categories = data?.thematicAreas?.results; + const actionPending = createPending || updatePending || deletePending; + + const handleResult = useCallback(( + ok: boolean | undefined, + successMessage: string, + ) => { + if (!ok) { + alert.show(errorMessage, { variant: 'danger' }); + return; + } + setCategoryName(undefined); + setEditingId(undefined); + reExecuteQuery({ requestPolicy: 'network-only' }); + onCategoriesChange(); + alert.show(successMessage, { variant: 'success' }); + }, [alert, reExecuteQuery, onCategoriesChange]); + + const handleSave = useCallback(() => { + const name = categoryName?.trim(); + if (!name) { + return; + } + if (isDefined(editingId)) { + updateThematicArea({ id: editingId, data: { name } }).then((resp) => { + handleResult(resp.data?.updateThematicArea?.ok, 'Category updated successfully'); + }).catch(() => { + alert.show(errorMessage, { variant: 'danger' }); + }); + } else { + createThematicArea({ data: { name } }).then((resp) => { + handleResult(resp.data?.createThematicArea?.ok, 'Category added successfully'); + }).catch(() => { + alert.show(errorMessage, { variant: 'danger' }); + }); + } + }, [categoryName, editingId, createThematicArea, updateThematicArea, handleResult, alert]); + + const handleEdit = useCallback((id: string) => { + setEditingId(id); + setCategoryName(categories?.find((item) => item.id === id)?.name); + }, [categories]); + + const handleCancelEdit = useCallback(() => { + setEditingId(undefined); + setCategoryName(undefined); + }, []); + + const handleDelete = useCallback((id: string) => { + deleteThematicArea({ id }).then((resp) => { + handleResult(resp.data?.deleteThematicArea?.ok, 'Category deleted successfully'); + }).catch(() => { + alert.show(errorMessage, { variant: 'danger' }); + }); + }, [deleteThematicArea, handleResult, alert]); + + const rendererParams = useCallback((_: string, category: ThematicArea) => ({ + category, + onEdit: handleEdit, + onDelete: handleDelete, + disabled: actionPending, + }), [handleEdit, handleDelete, actionPending]); + + const isEditing = isDefined(editingId); + + return ( + + {isEditing && ( + + + + )} + + {isEditing ? : } + + + )} + > + + + )} + > + + + + + ); +} + +export default CategoryModal; diff --git a/app/views/DataAndReports/DataAndReportsFilters/index.tsx b/app/views/DataAndReports/DataAndReportsFilters/index.tsx new file mode 100644 index 0000000..e395421 --- /dev/null +++ b/app/views/DataAndReports/DataAndReportsFilters/index.tsx @@ -0,0 +1,45 @@ +import { + SelectInput, + TextInput, +} from '@ifrc-go/ui'; +import { type EntriesAsList } from '@togglecorp/toggle-form'; + +import { type ThematicAreasQuery } from '#generated/types/graphql'; +import { + idSelector, + nameSelector, +} from '#utils/common'; + +import type { DataAndReportsFilterType } from '../index'; + +type ThematicAreaOption = NonNullable; + +export interface Props { + value: DataAndReportsFilterType; + onChange: (...args: EntriesAsList) => void; + thematicAreaOptions: ThematicAreaOption[] | undefined; +} + +function DataAndReportsFilters({ value, onChange, thematicAreaOptions }: Props) { + return ( + <> + + + + ); +} + +export default DataAndReportsFilters; diff --git a/app/views/DataAndReports/DataAndReportsForm/index.tsx b/app/views/DataAndReports/DataAndReportsForm/index.tsx new file mode 100644 index 0000000..3a95489 --- /dev/null +++ b/app/views/DataAndReports/DataAndReportsForm/index.tsx @@ -0,0 +1,511 @@ +import { + useCallback, + useEffect, + useMemo, +} from 'react'; +import { useParams } from 'react-router'; +import { + BlockLoading, + Button, + Container, + DateInput, + Description, + Image, + InlineLayout, + InputSection, + ListView, + RadioInput, + RawFileInput, + SelectInput, + TextInput, +} from '@ifrc-go/ui'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; +import { + createSubmitHandler, + getErrorObject, + type ObjectSchema, + type PartialForm, + removeNull, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import EmbedPreview from '#components/EmbedPreview'; +import NonFieldError from '#components/NonFieldError'; +import RegionSelectInput from '#components/RegionSelectInput'; +import { + AdminAreaLevel, + ReportContentType, + type ReportCreateInput, + ReportTypeEnum, + type ReportUpdateInput, + ReportVisibility, + useCreateReportMutation, + useReportDetailQuery, + useReportEnumsQuery, + useThematicAreasQuery, + useUpdateReportMutation, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import useRouting from '#hooks/useRouting'; +import { + errorMessage, + idSelector, + keySelector, + labelSelector, + nameSelector, + omitKeys, + safeUrlCondition, + transformToFormError, +} from '#utils/common'; + +type PartialFormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const reportSchema: FormSchema = { + fields: (value): FormSchemaFields => { + const baseFields: FormSchemaFields = { + title: { + required: true, + requiredValidation: requiredStringCondition, + }, + contentType: { + required: true, + }, + description: {}, + coverImage: {}, + disasterType: {}, + owner: {}, + region: {}, + reportType: { + required: true, + }, + visibility: { + required: true, + }, + thematicArea: { + required: true, + }, + publishedAt: {}, + }; + + if (value?.contentType === ReportContentType.Iframe) { + return { + ...baseFields, + iframeUrl: { + validations: [safeUrlCondition], + }, + }; + } + + return { + ...baseFields, + file: {}, + }; + }, +}; + +const defaultFormValue: PartialFormType = { + contentType: ReportContentType.File, + reportType: ReportTypeEnum.Report, + visibility: ReportVisibility.Public, +}; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + +function getFileFields( + file: File | null | undefined, + coverImage: File | null | undefined, +) { + return { + ...(isDefined(file) ? { file } : {}), + ...(isDefined(coverImage) ? { coverImage } : {}), + }; +} + +function DataAndReportsForm() { + const { id } = useParams(); + const navigate = useRouting(); + const alert = useAlert(); + + const { + setFieldValue, + error: formError, + value, + validate, + setError, + setValue, + } = useForm(reportSchema, { value: defaultFormValue }); + + const [{ data: detailData, fetching: detailFetching }] = useReportDetailQuery({ + variables: { id: isDefined(id) ? id : '' }, + pause: isNotDefined(id), + }); + + const [{ data: enumsData }] = useReportEnumsQuery(); + const [{ data: thematicAreasData }] = useThematicAreasQuery(); + + const [{ fetching: creating }, createReport] = useCreateReportMutation(); + const [{ fetching: updating }, updateReport] = useUpdateReportMutation(); + + const pending = creating || updating || detailFetching; + + const { + ReportVisibility: visibilityOptions, + ReportContentType: contentTypeOptions, + } = enumsData?.enums ?? {}; + + const thematicAreaOptions = thematicAreasData?.thematicAreas?.results; + + const isIframe = value.contentType === ReportContentType.Iframe; + + const existingCoverImageUrl = detailData?.report?.coverImage?.url; + + const fileName = value.file instanceof File + ? value.file.name + : detailData?.report?.file?.name?.split('/').pop(); + + const coverImagePreview = useMemo(() => { + if (value.coverImage instanceof File) { + return URL.createObjectURL(value.coverImage); + } + return existingCoverImageUrl; + }, [value.coverImage, existingCoverImageUrl]); + + useEffect(() => () => { + if (value.coverImage instanceof File && coverImagePreview) { + URL.revokeObjectURL(coverImagePreview); + } + }, [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; + } + setFieldValue(file, name); + }, + [alert, setFieldValue], + ); + + const handleResult = useCallback(( + result: { + ok?: boolean | null; + errors?: Parameters[0] | null; + } | undefined | null, + successMessage: string, + ) => { + if (isDefined(result) && result.ok) { + navigate('dataAndReports'); + alert.show(successMessage, { variant: 'success' }); + } else if (isDefined(result) && isDefined(result.errors)) { + setError(transformToFormError(result.errors)); + alert.show(errorMessage, { variant: 'danger' }); + } else { + alert.show(errorMessage, { variant: 'danger' }); + } + }, [navigate, alert, setError]); + + const handleCreate = useCallback(async (formValues: PartialFormType) => { + const { file, coverImage, ...rest } = formValues; + + const res = await createReport({ + data: { + ...removeNull(rest), + ...getFileFields(file, coverImage), + } as ReportCreateInput, + }); + + handleResult(res.data?.createReport, 'Report created successfully'); + }, [createReport, handleResult]); + + const handleUpdate = useCallback(async (formValues: PartialFormType) => { + if (isNotDefined(id)) { + return; + } + const { file, coverImage, ...rest } = formValues; + + const res = await updateReport({ + id, + data: { + ...omitKeys(removeNull(rest), ['contentType', 'iframeUrl']), + ...getFileFields(file, coverImage), + } as ReportUpdateInput, + }); + + handleResult(res.data?.updateReport, 'Report updated successfully'); + }, [id, updateReport, handleResult]); + + const handleFormSubmit = useCallback( + () => createSubmitHandler( + validate, + setError, + isDefined(id) ? handleUpdate : handleCreate, + )(), + [validate, setError, id, handleUpdate, handleCreate], + ); + + const handleCancel = useCallback(() => { + navigate('dataAndReports'); + }, [navigate]); + + const error = getErrorObject(formError); + + useEffect(() => { + if (isNotDefined(detailData?.report)) { + return; + } + const { + regionId, + thematicAreaId, + ...otherValues + } = removeNull(detailData.report); + + delete (otherValues as { coverImage?: unknown }).coverImage; + delete (otherValues as { file?: unknown }).file; + setValue({ + ...otherValues, + region: regionId ?? undefined, + thematicArea: thematicAreaId, + }); + }, [detailData, setValue]); + + if (detailFetching) { + return ( + + ); + } + + return ( + + + + + )} + > + + + + + + + + + + + + + + + {isDefined(value.coverImage) || isDefined(coverImagePreview) + ? 'Change cover image' + : 'Upload cover image'} + + {isDefined(coverImagePreview) && ( + Cover image preview + )} + + + + + {isIframe ? ( + + + + ) : ( + + + {isDefined(fileName) ? 'Change file' : 'Upload file'} + + )} + > + + {isDefined(fileName) ? fileName : 'Please upload a file'} + + + + )} + + + + + + + + + + + + + + + + + {isIframe && ( + + )} + + + ); +} + +export default DataAndReportsForm; diff --git a/app/views/DataAndReports/index.tsx b/app/views/DataAndReports/index.tsx index 3dcc9aa..14aa839 100644 --- a/app/views/DataAndReports/index.tsx +++ b/app/views/DataAndReports/index.tsx @@ -1,8 +1,245 @@ +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { AddFillIcon } from '@ifrc-go/icons'; +import { + Button, + Container, + ListView, + Pager, + Table, +} from '@ifrc-go/ui'; +import { + createDateColumn, + createElementColumn, + createStringColumn, +} from '@ifrc-go/ui/utils'; +import { listToMap } from '@togglecorp/fujs'; + +import EditDeleteActions, { type Props as EditDeleteActionsProps } from '#components/EditDeleteActions'; +import { + type ReportFilter, + type ReportsQuery, + useDeleteReportMutation, + useReportsQuery, + useThematicAreasQuery, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import useFilterState from '#hooks/useFilterState'; +import useRouting from '#hooks/useRouting'; +import { + errorMessage, + idSelector, +} from '#utils/common'; + +import CategoryModal from './CategoryModal'; +import DataAndReportsFilters from './DataAndReportsFilters'; + +type ReportsListItem = NonNullable['results'][number]> & { no: string }; + +export type DataAndReportsFilterType = Pick; + +const defaultFilter: DataAndReportsFilterType = { + search: undefined, + thematicAreaId: undefined, +}; + +// NOTE: category options/map need every thematic area, so fetch a single large page. +const THEMATIC_AREA_OPTIONS_LIMIT = 100; + function DataAndReports() { + const { + filter, + rawFilter, + filtered, + setFilterField, + page, + setPage, + limit, + offset, + } = useFilterState({ + filter: defaultFilter, + }); + + const alert = useAlert(); + const navigate = useRouting(); + + const [categoryModalShown, setCategoryModalShown] = useState(false); + + const [ + { data: thematicAreasData }, + reExecuteThematicAreas, + ] = useThematicAreasQuery({ + variables: { pagination: { limit: THEMATIC_AREA_OPTIONS_LIMIT, offset: 0 } }, + }); + + const queryVariables = useMemo(() => ({ + pagination: { + limit, + offset, + }, + filters: { + search: filter.search || undefined, + thematicAreaId: filter.thematicAreaId || undefined, + }, + }), [limit, offset, filter]); + + const [{ fetching, data }, reExecuteQuery] = useReportsQuery({ variables: queryVariables }); + const [, deleteReport] = useDeleteReportMutation(); + + const thematicAreaMap = useMemo(() => ( + listToMap( + thematicAreasData?.thematicAreas?.results ?? [], + (area) => area.id, + (area) => area.name, + ) + ), [thematicAreasData]); + + const thematicAreaOptions = thematicAreasData?.thematicAreas?.results; + + const tableData: ReportsListItem[] = useMemo(() => ( + (data?.reports?.results ?? []).map((report, index) => ({ + ...report, + no: String((page - 1) * limit + index + 1), + })) + ), [page, data, limit]); + + const onDeleteClick = useCallback( + (id: string) => { + deleteReport({ id }).then((resp) => { + const result = resp.data?.deleteReport; + if (result?.ok) { + reExecuteQuery(); + alert.show('Report deleted successfully', { variant: 'success' }); + } else { + alert.show(errorMessage, { variant: 'danger' }); + } + }).catch(() => { + alert.show(errorMessage, { variant: 'danger' }); + }); + }, + [deleteReport, reExecuteQuery, alert], + ); + + const columns = useMemo(() => [ + createStringColumn( + 'no', + 'No.', + (item) => item.no, + ), + createDateColumn( + 'createdAt', + 'Created At', + (item) => item.createdAt, + ), + createStringColumn( + 'title', + 'Title', + (item) => item.title, + ), + createStringColumn( + 'owner', + 'Created By', + (item) => item.owner, + ), + createStringColumn( + 'category', + 'Category', + (item) => thematicAreaMap[item.thematicAreaId], + ), + createDateColumn( + 'updatedAt', + 'Updated At', + (item) => item.updatedAt, + ), + createStringColumn( + 'status', + 'Status', + (item) => item.visibilityDisplay, + ), + createElementColumn( + 'actions', + '', + EditDeleteActions, + (_, datum) => ({ + id: datum.id, + onDelete: onDeleteClick, + itemTitle: datum.title, + to: 'editDataAndReports', + }), + { columnWidth: 150 }, + ), + ], [onDeleteClick, thematicAreaMap]); + + const handleViewCategoryClick = useCallback(() => { + setCategoryModalShown(true); + }, []); + const handleCategoryModalClose = useCallback(() => { + setCategoryModalShown(false); + }, []); + const handleCategoriesChange = useCallback(() => { + reExecuteThematicAreas({ requestPolicy: 'network-only' }); + }, [reExecuteThematicAreas]); + const handleCreateClick = useCallback(() => { + navigate('createDataAndReports'); + }, [navigate]); + return ( -
- DataAndReports -
+ + )} + headerActions={( + + + + + )} + footerActions={( + + )} + > + + {categoryModalShown && ( + + )} + ); } diff --git a/app/views/DataAndReports/query.ts b/app/views/DataAndReports/query.ts new file mode 100644 index 0000000..acd28a1 --- /dev/null +++ b/app/views/DataAndReports/query.ts @@ -0,0 +1,168 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { gql } from 'urql'; + +const REPORTS = gql` + query Reports($pagination: OffsetPaginationInput, $filters: ReportFilter) { + reports(pagination: $pagination, filters: $filters) { + results { + id + createdAt + title + owner + reportType + reportTypeDisplay + thematicAreaId + updatedAt + visibility + visibilityDisplay + } + totalCount + } + } +`; + +const DELETE_REPORT = gql` + mutation DeleteReport($id: ID!) { + deleteReport(id: $id) { + ... on ReportTypeMutationResponseType { + errors + ok + result { + id + } + } + } + } +`; + +const THEMATIC_AREAS = gql` + query ThematicAreas($pagination: OffsetPaginationInput, $filters: ThematicAreaFilter) { + thematicAreas(pagination: $pagination, filters: $filters) { + totalCount + results { + id + name + createdAt + updatedAt + } + } + } +`; + +const CREATE_THEMATIC_AREA = gql` + mutation CreateThematicArea($data: ThematicAreaInput!) { + createThematicArea(data: $data) { + ... on ThematicAreaTypeMutationResponseType { + errors + ok + result { + id + name + } + } + } + } +`; + +const UPDATE_THEMATIC_AREA = gql` + mutation UpdateThematicArea($id: ID!, $data: ThematicAreaInput!) { + updateThematicArea(id: $id, data: $data) { + ... on ThematicAreaTypeMutationResponseType { + errors + ok + result { + id + name + } + } + } + } +`; + +const DELETE_THEMATIC_AREA = gql` + mutation DeleteThematicArea($id: ID!) { + deleteThematicArea(id: $id) { + ... on ThematicAreaTypeMutationResponseType { + errors + ok + result { + id + } + } + } + } +`; + +const REPORT_ENUMS = gql` + query ReportEnums { + enums { + ReportType { + key + label + } + ReportVisibility { + key + label + } + ReportContentType { + key + label + } + } + } +`; + +const REPORT_DETAIL = gql` + query ReportDetail($id: ID!) { + report(id: $id) { + id + title + description + disasterType + contentType + iframeUrl + owner + visibility + reportType + thematicAreaId + regionId + publishedAt + coverImage { + url + name + } + file { + url + name + } + } + } +`; + +const CREATE_REPORT = gql` + mutation CreateReport($data: ReportCreateInput!) { + createReport(data: $data) { + ... on ReportTypeMutationResponseType { + errors + ok + result { + id + } + } + } + } +`; + +const UPDATE_REPORT = gql` + mutation UpdateReport($id: ID!, $data: ReportUpdateInput!) { + updateReport(id: $id, data: $data) { + ... on ReportTypeMutationResponseType { + errors + ok + result { + id + } + } + } + } +`; diff --git a/backend b/backend index c3f7ecd..19a4583 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit c3f7ecda7263f09339364dd2bd9c3f65d6a88956 +Subproject commit 19a4583618f4c2ef3ba5861eed905fb299e6830d From c26e9ba85ad31c4330e1b4e42da75f6a1b808cc4 Mon Sep 17 00:00:00 2001 From: amrit Date: Fri, 26 Jun 2026 11:17:39 +0545 Subject: [PATCH 2/2] fixup! feat(data-and-reports): add data and reports module --- .../DataAndReports/CategoryModal/index.tsx | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/app/views/DataAndReports/CategoryModal/index.tsx b/app/views/DataAndReports/CategoryModal/index.tsx index d1c6470..2f2ff6c 100644 --- a/app/views/DataAndReports/CategoryModal/index.tsx +++ b/app/views/DataAndReports/CategoryModal/index.tsx @@ -18,7 +18,10 @@ import { Pager, TextInput, } from '@ifrc-go/ui'; -import { isDefined } from '@togglecorp/fujs'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; import { type ThematicAreasQuery, @@ -152,25 +155,29 @@ function CategoryModal(props: Props) { alert.show(successMessage, { variant: 'success' }); }, [alert, reExecuteQuery, onCategoriesChange]); - const handleSave = useCallback(() => { + const handleCreate = useCallback(() => { const name = categoryName?.trim(); if (!name) { return; } - if (isDefined(editingId)) { - updateThematicArea({ id: editingId, data: { name } }).then((resp) => { - handleResult(resp.data?.updateThematicArea?.ok, 'Category updated successfully'); - }).catch(() => { - alert.show(errorMessage, { variant: 'danger' }); - }); - } else { - createThematicArea({ data: { name } }).then((resp) => { - handleResult(resp.data?.createThematicArea?.ok, 'Category added successfully'); - }).catch(() => { - alert.show(errorMessage, { variant: 'danger' }); - }); + createThematicArea({ data: { name } }).then((resp) => { + handleResult(resp.data?.createThematicArea?.ok, 'Category added successfully'); + }).catch(() => { + alert.show(errorMessage, { variant: 'danger' }); + }); + }, [categoryName, createThematicArea, handleResult, alert]); + + const handleUpdate = useCallback(() => { + const name = categoryName?.trim(); + if (!name || isNotDefined(editingId)) { + return; } - }, [categoryName, editingId, createThematicArea, updateThematicArea, handleResult, alert]); + updateThematicArea({ id: editingId, data: { name } }).then((resp) => { + handleResult(resp.data?.updateThematicArea?.ok, 'Category updated successfully'); + }).catch(() => { + alert.show(errorMessage, { variant: 'danger' }); + }); + }, [categoryName, editingId, updateThematicArea, handleResult, alert]); const handleEdit = useCallback((id: string) => { setEditingId(id); @@ -226,7 +233,7 @@ function CategoryModal(props: Props) { title={isEditing ? 'Update category' : 'Add category'} variant="primary" disabled={!categoryName?.trim() || actionPending} - onClick={handleSave} + onClick={isEditing ? handleUpdate : handleCreate} > {isEditing ? : }