Skip to content

Commit

Permalink
refactor(frontend): organization management pages (#2097)
Browse files Browse the repository at this point in the history
* fix(uploadArea): refactor uploadArea component

* refactor(formUpdateTab): refactor code, show error if no form upload

* feat(isEmpty): isEmpty function add

* feat(utilFunctions): convertFileUrlToArray, getFileNameFromURL function add

* fix(refactor): use uploadArea component for logo upload

* refactor(createEditOrganizationForm): define api url to top

* feat(createEditOrganizationForm): associated email form field add

* fix(organizationForm): associated email add

* fix(organisation_schemas): associated_email add

* feat(createEditOrganizationForm): show toast if no change in form

* fix(organizationDetailsValidation): add email validation to organization form

* fix(organizationService): fix state early state clear issue

* fix(organizationService): show error toast on error

* refactor(routes): update url paths for simplicity and uniformity
  • Loading branch information
NSUWAL123 authored Jan 21, 2025
1 parent 0da806c commit 4d43ce5
Show file tree
Hide file tree
Showing 17 changed files with 192 additions and 152 deletions.
2 changes: 2 additions & 0 deletions src/backend/app/organisations/organisation_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def parse_organisation_input(
created_by: Optional[int] = Form(None),
community_type: CommunityType = Form(None),
description: Optional[str] = Form(None),
associated_email: Optional[str] = Form(None),
url: Optional[str] = Form(None),
type: OrganisationType = Form(None, alias="type"),
odk_central_url: Optional[str] = Form(None),
Expand All @@ -86,6 +87,7 @@ def parse_organisation_input(
slug=slug,
created_by=created_by,
description=description,
associated_email=associated_email,
url=url,
community_type=community_type,
type=type,
Expand Down
20 changes: 13 additions & 7 deletions src/frontend/src/api/OrganisationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,9 @@ export const MyOrganisationDataService = (url: string) => {

export const PostOrganisationDataService = (url: string, payload: any) => {
return async (dispatch: AppDispatch) => {
dispatch(OrganisationAction.SetOrganisationFormData({}));
dispatch(OrganisationAction.PostOrganisationDataLoading(true));

const postOrganisationData = async (url, payload) => {
dispatch(OrganisationAction.SetOrganisationFormData(payload));

try {
const generateApiFormData = new FormData();
appendObjectToFormData(generateApiFormData, payload);
Expand All @@ -99,6 +96,8 @@ export const PostOrganisationDataService = (url: string, payload: any) => {

dispatch(OrganisationAction.PostOrganisationDataLoading(false));
dispatch(OrganisationAction.postOrganisationData(resp));
dispatch(OrganisationAction.SetOrganisationFormData({}));

dispatch(
CommonActions.SetSnackBar({
open: true,
Expand Down Expand Up @@ -132,20 +131,26 @@ export const GetIndividualOrganizationService = (url: string) => {
const getOrganisationDataResponse = await axios.get(url);
const response: GetOrganisationDataModel = getOrganisationDataResponse.data;
dispatch(OrganisationAction.SetIndividualOrganization(response));
} catch (error) {}
} catch (error) {
dispatch(
CommonActions.SetSnackBar({
open: true,
message: error.response.data.detail || 'Failed to fetch organization.',
variant: 'error',
duration: 2000,
}),
);
}
};
await getOrganisationData(url);
};
};

export const PatchOrganizationDataService = (url: string, payload: any) => {
return async (dispatch: AppDispatch) => {
dispatch(OrganisationAction.SetOrganisationFormData({}));
dispatch(OrganisationAction.PostOrganisationDataLoading(true));

const patchOrganisationData = async (url, payload) => {
dispatch(OrganisationAction.SetOrganisationFormData(payload));

try {
const generateApiFormData = new FormData();
appendObjectToFormData(generateApiFormData, payload);
Expand All @@ -159,6 +164,7 @@ export const PatchOrganizationDataService = (url: string, payload: any) => {
const resp: GetOrganisationDataModel = patchOrganisationData.data;
dispatch(OrganisationAction.PostOrganisationDataLoading(false));
dispatch(OrganisationAction.postOrganisationData(resp));
dispatch(OrganisationAction.SetOrganisationFormData({}));
dispatch(
CommonActions.SetSnackBar({
open: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ApproveOrganizationHeader = () => {
</div>
<div
className="hover:fmtm-bg-gray-200 fmtm-rounded-full fmtm-p-2 fmtm-duration-300 fmtm-cursor-pointer"
onClick={() => navigate('/organisation')}
onClick={() => navigate('/organization')}
>
<AssetModules.CloseIcon />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const OrganizationForm = () => {
if (organizationApprovalSuccess) {
dispatch(OrganisationAction.SetOrganisationFormData({}));
dispatch(OrganisationAction.SetOrganizationApprovalStatus(false));
navigate('/organisation');
navigate('/organization');
}
}, [organizationApprovalSuccess]);

Expand Down Expand Up @@ -82,6 +82,15 @@ const OrganizationForm = () => {
fieldType="text"
disabled
/>
<InputTextField
id="associated_email"
name="associated_email"
label="Email"
value={organisationFormData?.associated_email}
onChange={() => {}}
fieldType="text"
disabled
/>
<TextArea
id="description"
name="description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const ConsentDetailsForm = () => {
btnText="Cancel"
btnType="other"
className="fmtm-font-bold"
onClick={() => navigate('/organisation')}
onClick={() => navigate('/organization')}
/>
<Button btnText="NEXT" btnType="primary" className="fmtm-font-bold" onClick={handleSubmit} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect } from 'react';
import Button from '@/components/common/Button';
import InputTextField from '@/components/common/InputTextField';
import TextArea from '@/components/common/TextArea';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { OrganisationAction } from '@/store/slices/organisationSlice';
import useForm from '@/hooks/useForm';
import AssetModules from '@/shared/AssetModules';
import OrganizationDetailsValidation from '@/components/CreateEditOrganization/validation/OrganizationDetailsValidation';
import RadioButton from '@/components/common/RadioButton';
import {
Expand All @@ -18,6 +17,10 @@ import InstructionsSidebar from '@/components/CreateEditOrganization/Instruction
import { CustomCheckbox } from '@/components/common/Checkbox';
import { organizationTypeOptionsType } from '@/models/organisation/organisationModel';
import { useAppDispatch, useAppSelector } from '@/types/reduxTypes';
import UploadArea from '@/components/common/UploadArea';
import { CommonActions } from '@/store/slices/CommonSlice';

const API_URL = import.meta.env.VITE_API_URL;

const organizationTypeOptions: organizationTypeOptionsType[] = [
{ name: 'osm_community', value: 'OSM_COMMUNITY', label: 'OSM Community' },
Expand All @@ -31,43 +34,48 @@ const CreateEditOrganizationForm = ({ organizationId }: { organizationId: string
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [searchParams, setSearchParams] = useSearchParams();
const inputFileRef = useRef<any>(null);
const organisationFormData = useAppSelector((state) => state.organisation.organisationFormData);
const postOrganisationDataLoading = useAppSelector((state) => state.organisation.postOrganisationDataLoading);
const postOrganisationData = useAppSelector((state) => state.organisation.postOrganisationData);
const [previewSource, setPreviewSource] = useState<any>('');

const submission = () => {
if (!organizationId) {
const { fillODKCredentials, ...filteredValues } = values;
dispatch(PostOrganisationDataService(`${import.meta.env.VITE_API_URL}/organisation`, filteredValues));
dispatch(
PostOrganisationDataService(`${API_URL}/organisation`, {
...filteredValues,
logo: filteredValues.logo ? filteredValues.logo?.[0].file : null,
}),
);
} else {
const { fillODKCredentials, ...filteredValues } = values;
const changedValues = diffObject(organisationFormData, filteredValues);
let changedValues = diffObject(organisationFormData, filteredValues);
if (changedValues.logo) {
changedValues = {
...changedValues,
logo: changedValues.logo?.length > 0 ? changedValues.logo?.[0].file : null,
};
}
if (Object.keys(changedValues).length > 0) {
dispatch(PatchOrganizationDataService(`${API_URL}/organisation/${organizationId}`, changedValues));
} else {
dispatch(
PatchOrganizationDataService(`${import.meta.env.VITE_API_URL}/organisation/${organizationId}`, changedValues),
CommonActions.SetSnackBar({
open: true,
message: 'Organization details up to date',
variant: 'info',
duration: 2000,
}),
);
}
}
};

const { handleSubmit, handleChange, handleCustomChange, values, errors }: any = useForm(
organisationFormData,
submission,
OrganizationDetailsValidation,
);

const previewFile = (file: File) => {
const reader = new FileReader();
reader.readAsDataURL(file); //reads file as data url (base64 encoding)
reader.onload = () => {
if (reader) {
setPreviewSource(reader?.result);
}
};
};

// redirect to manage-org page after post success
useEffect(() => {
if (postOrganisationData) {
Expand All @@ -85,14 +93,14 @@ const CreateEditOrganizationForm = ({ organizationId }: { organizationId: string
if (searchParams.get('popup') === 'true') {
window.close();
} else {
navigate('/organisation');
navigate('/organization');
}
}
}, [postOrganisationData]);

useEffect(() => {
if (organizationId) {
dispatch(GetIndividualOrganizationService(`${import.meta.env.VITE_API_URL}/organisation/${organizationId}`));
dispatch(GetIndividualOrganizationService(`${API_URL}/organisation/${organizationId}`));
}
}, [organizationId]);

Expand Down Expand Up @@ -151,6 +159,16 @@ const CreateEditOrganizationForm = ({ organizationId }: { organizationId: string
errorMsg={errors.url}
/>
)}
<InputTextField
id="associated_email"
name="associated_email"
label="Email"
value={values?.associated_email}
onChange={handleChange}
fieldType="text"
required
errorMsg={errors.associated_email}
/>
<TextArea
id="description"
name="description"
Expand Down Expand Up @@ -218,43 +236,15 @@ const CreateEditOrganizationForm = ({ organizationId }: { organizationId: string
required
/>
)}
<div className="flex items-center">
<p className="fmtm-text-[1rem] fmtm-mb-2 fmtm-font-semibold">Upload Logo</p>
<div className="fmtm-flex fmtm-flex-col fmtm-gap-5">
<input
ref={inputFileRef}
type="file"
className="fmtm-max-w-[250px]"
onChange={(e) => {
handleCustomChange('logo', e.target?.files?.[0]);
if (e.target?.files?.[0]) {
previewFile(e.target?.files?.[0]);
}
}}
accept="image/png, image/gif, image/jpeg"
/>
{(previewSource || values.logo) && (
<div className="fmtm-relative fmtm-w-fit">
<div className="fmtm-absolute -fmtm-top-3 -fmtm-right-3" title="Remove Logo">
<AssetModules.DeleteIcon
style={{ fontSize: '28px' }}
className="fmtm-text-primaryRed hover:fmtm-text-red-700 fmtm-cursor-pointer"
onClick={() => {
inputFileRef.current.value = '';
handleCustomChange('logo', '');
setPreviewSource('');
}}
/>
</div>
<img
src={previewSource ? previewSource : values.logo ? values.logo : ''}
alt=""
className="fmtm-h-[100px] fmtm-rounded-sm fmtm-border-[1px]"
/>
</div>
)}
</div>
</div>
<UploadArea
title="Upload Logo"
label="Please upload .png, .gif, .jpeg"
data={values?.logo}
onUploadFile={(updatedFiles) => {
handleCustomChange('logo', updatedFiles);
}}
acceptedInput="image/*"
/>
</div>

<div className="fmtm-flex fmtm-items-center fmtm-justify-center fmtm-gap-6 fmtm-mt-8 lg:fmtm-mt-16">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const CreateEditOrganizationHeader = ({ organizationId }: { organizationId: stri
</div>
<div
className="hover:fmtm-bg-gray-200 fmtm-rounded-full fmtm-p-2 fmtm-duration-300 fmtm-cursor-pointer"
onClick={() => navigate('/organisation')}
onClick={() => navigate('/organization')}
>
<AssetModules.CloseIcon />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface OrganisationValues {
osm_profile: string;
community_type: string;
fillODKCredentials: boolean;
associated_email: string;
}
interface ValidationErrors {
logo?: string;
Expand All @@ -27,8 +28,11 @@ interface ValidationErrors {
osm_profile?: string;
community_type?: string;
fillODKCredentials?: boolean;
associated_email?: string;
}

const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function OrganizationDetailsValidation(values: OrganisationValues) {
const errors: ValidationErrors = {};

Expand All @@ -55,6 +59,12 @@ function OrganizationDetailsValidation(values: OrganisationValues) {
errors.odk_central_url = 'Invalid URL.';
}

if (isInputEmpty(values?.associated_email)) {
errors.associated_email = 'Email is Required.';
} else if (!emailPattern.test(values?.associated_email)) {
errors.associated_email = 'Invalid Email.';
}

if (values?.fillODKCredentials && isInputEmpty(values.odk_central_url)) {
errors.odk_central_url = 'ODK central URL is Required.';
}
Expand Down
Loading

0 comments on commit 4d43ce5

Please sign in to comment.