Skip to content

Commit

Permalink
Mn.approve reject modal2 (#363)
Browse files Browse the repository at this point in the history
* Added component for filter

* Add skeleton component, params, api route to client, basic outline

* Fix compilation errors

* Fix site id

* Add list view of image

* Cannot read properties of undefined map

* Start selection

* Tested filter site images

* Fix null

* Cleanup

* Add frontend implementation of approving and rejecting images

* Address review comments

* fix translation changes

* Add loading functionality and clear filters functionality

* working: translations + styling + tests

* working: need to finalize design

* tests + cleanup

* tests

* disable submit when no reason

* initial value

* remove usecallback + switch to dropdown

* Fix so that the table works with no unapproved images

* correct reject route

* Added reason

* Finished most of the visuals

Finished most of the UI for the grid view and the modals

* Finished approve/reject modal

* Update protectedApiClient.ts

* fix json + duped button (#364)

* pr comments: message, select checkbox, dynamic styles

* translations

---------

Co-authored-by: SurabhiKeesara <surabhi.keesara@gmail>
Co-authored-by: Avery Huang <[email protected]>
Co-authored-by: Avery Huang <[email protected]>
  • Loading branch information
4 people authored Jul 2, 2024
1 parent 4189b0d commit 7bda8d8
Show file tree
Hide file tree
Showing 8 changed files with 625 additions and 41 deletions.
63 changes: 63 additions & 0 deletions src/api/test/protectedApiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2308,6 +2308,69 @@ describe('Admin Protected Client Routes', () => {
});
});

describe('filterSiteImages', () => {
it('makes the right request', async () => {
const response = '';

const params = {
submittedStart: null,
submittedEnd: null,
neighborhoods: [0, 1, 2],
siteIds: [0, 1, 2, 3],
};

nock(BASE_URL)
.get(ParameterizedAdminApiRoutes.FILTER_SITE_IMAGES(params))
.reply(200, response);

const result = await ProtectedApiClient.filterSiteImages(params);

expect(result).toEqual(response);
});

it('makes a bad request', async () => {
const response = 'Invalid dates given!';

const params = {
submittedStart: 'invalid date',
submittedEnd: null,
neighborhoods: [0, 1, 2],
siteIds: [0, 1, 2, 3],
};

nock(BASE_URL)
.get(ParameterizedAdminApiRoutes.FILTER_SITE_IMAGES(params))
.reply(400, response);

const result = await ProtectedApiClient.filterSiteImages(params).catch(
(err) => err.response.data,
);

expect(result).toEqual(response);
});

it('makes an unauthorized request ', async () => {
const response = 'Must be an admin';

const params = {
submittedStart: null,
submittedEnd: null,
neighborhoods: [0, 1, 2],
siteIds: [0, 1, 2, 3],
};

nock(BASE_URL)
.get(ParameterizedAdminApiRoutes.FILTER_SITE_IMAGES(params))
.reply(401, response);

const result = await ProtectedApiClient.filterSiteImages(params).catch(
(err) => err.response.data,
);

expect(result).toEqual(response);
});
});

describe('uploadImage', () => {
let imageToUpload: string | ArrayBuffer;

Expand Down
259 changes: 259 additions & 0 deletions src/components/imageApprovalModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import { Modal, Space, Button, Row, Col, Input, message } from 'antd';
import React, {
CSSProperties,
Dispatch,
SetStateAction,
useEffect,
useState,
} from 'react';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { FilterImageTableData } from '../../containers/reviewImages/types';
import protectedApiClient from '../../api/protectedApiClient';
import { useTranslation } from 'react-i18next';
import { n } from '../../utils/stringFormat';
import { site } from '../../constants';
const { TextArea } = Input;

interface ImageApprovalModal {
visible: boolean;
onClose: () => void;
tableData: FilterImageTableData | null;
approvedOrRejectedImageIds: number[];
setApprovedOrRejectedImageIds: Dispatch<SetStateAction<number[]>>;
allData: FilterImageTableData[];
setSelectedImage: Dispatch<SetStateAction<FilterImageTableData | null>>;
}

const ImageApprovalModal: React.FC<ImageApprovalModal> = (props) => {
const { t } = useTranslation(n(site, ['admin']), {
nsMode: 'fallback',
keyPrefix: 'review_images',
});

const { t: tForms } = useTranslation(n(site, ['forms']), {
nsMode: 'fallback',
});

const [open, setOpen] = useState(props.visible);
const [data, setData] = useState(props.tableData);
const [isRejectionTextOpen, setIsRejectionTextOpen] = useState(false);
const [rejectionReason, setRejectionReason] = useState('');
const [allData, setAllData] = useState(props.allData);

const close = () => {
setOpen(false);
props.onClose();
};

const openRejectionTextBox = () => {
setIsRejectionTextOpen(true);
};

const handleNextSubmission = () => {
const currentIndex = allData.findIndex(
(siteData) => siteData.key === data?.key,
);
const nextIndex = (currentIndex + 1) % allData.length;
const nextImage = allData[nextIndex];
setData(nextImage);
};

const footer = (
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Button style={{ color: 'gray' }} onClick={close}>
<LeftOutlined /> {t('modal.back')}
</Button>
<Button style={{ color: 'gray' }} onClick={handleNextSubmission}>
{t('modal.next')} <RightOutlined />
</Button>
</Space>
);

const rejectionReasonStyle = {
background: 'rgb(239, 239, 239)',
borderRadius: '5px',
marginTop: '5%',
padding: '3%',
paddingRight: '10%',
};

const StatusHeader = () => {
return (
<>
<h1 style={{ color: 'black', marginBottom: '-8px' }}>
{t('modal.status')}
</h1>
<div
style={{
display: 'flex',
width: '100%',
backgroundColor: 'rgb(232, 240, 215)',
padding: '2%',
borderRadius: '5px',
}}
>
<Button
type="primary"
style={{ flex: 1, marginRight: '8px', color: 'black' }}
onClick={onClickAccept}
>
{t('modal.approve')}
</Button>
<Button
type="primary"
style={{ flex: 1, color: 'black' }}
onClick={openRejectionTextBox}
>
{t('modal.reject')}
</Button>
</div>
</>
);
};

if (!data) {
return null;
}

const treeSummaryTextStyle: CSSProperties = {
whiteSpace: 'nowrap', // Prevent text from wrapping
};

async function onClickReject() {
const toReject: Promise<void>[] = [];
if (!data) {
return null;
}
toReject.push(protectedApiClient.rejectImage(data.key, rejectionReason));
Promise.all(toReject)
.then(() => {
props.setApprovedOrRejectedImageIds((prevIds) => [
...prevIds,
data.key,
]);
close();
})
.then(() => {
message.success(t('message.reject_success'));
})
.catch((err) => {
message.error(t('message.reject_error', { error: err.response.data }));
});
}

async function onClickAccept() {
const toApprove: Promise<void>[] = [];
if (!data) {
return null;
}
toApprove.push(protectedApiClient.approveImage(data.key));
Promise.all(toApprove)
.then(() => {
props.setApprovedOrRejectedImageIds((prevIds) => [
...prevIds,
data.key,
]);
close();
})
.then(() => {
message.success(t('message.approve_success'));
})
.catch((err) => {
message.error(t('message.approve_error', { error: err.response.data }));
});
}

function treeSummaryLine(lineName: string, lineItem: number | string) {
return (
<>
<Col span={12}>
<b>
<p style={treeSummaryTextStyle}>{lineName}</p>
</b>
</Col>
<Col span={12}>
<p style={treeSummaryTextStyle}>{lineItem}</p>
</Col>
</>
);
}

const TreeSummaryDisplay = () => {
return (
<div style={{ marginTop: '5%' }}>
<Row gutter={[24, 25]}>
<Col span={24}>
<Row gutter={25}>
{isRejectionTextOpen ? (
<></>
) : (
<>
{treeSummaryLine(t('summary.id'), data.siteId)}
{treeSummaryLine(t('summary.date'), data.dateSubmitted)}
{treeSummaryLine(t('summary.submitted'), data.submittedBy)}
</>
)}
{treeSummaryLine(t('summary.species'), data.species)}
{treeSummaryLine(t('summary.neighborhood'), data.neighborhood)}
{treeSummaryLine(t('summary.address'), data.address)}
</Row>
</Col>
</Row>
</div>
);
};

useEffect(() => {
setOpen(open);
}, [props.visible]);

return (
<Modal
open={open}
onOk={close}
onCancel={close}
footer={footer}
width={'40%'}
>
<Row gutter={[15, 0]}>
<Col span={12}>
<img
src={data.preview}
alt="Submission"
style={{
width: '100%',
border: '3px SOLID rgb(141, 164, 113)',
borderRadius: '5px',
}}
/>
</Col>
<Col span={12}>
<StatusHeader />
{isRejectionTextOpen ? (
<div style={rejectionReasonStyle}>
<h3 style={{ color: 'black' }}>{t('modal.form.prompt')}</h3>
<TextArea
placeholder={t('modal.form.placeholder')}
style={{ height: 50 }}
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
/>
<Button
type="primary"
style={{ marginTop: '3%' }}
onClick={onClickReject}
>
{tForms('submit')}
</Button>
</div>
) : (
<></>
)}
<TreeSummaryDisplay />
</Col>
</Row>
</Modal>
);
};

export default ImageApprovalModal;
4 changes: 2 additions & 2 deletions src/components/treePage/treeInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ export const TreeInfo: React.FC<TreeProps> = ({
isAdopted={isAdopted}
/>

{/* {treePresent && (
{treePresent && (
<UploadSiteImageButton siteEntryId={siteData.entries[0].id} />
)} */}
)}

<ReportSiteButton siteId={siteData.siteId} />

Expand Down
Loading

0 comments on commit 7bda8d8

Please sign in to comment.