feat: add deleting form functionality

This commit is contained in:
Ayobami
2025-07-04 01:28:52 +01:00
parent b4a7a56200
commit 3b46073ee8
11 changed files with 382 additions and 11 deletions
@@ -0,0 +1,50 @@
import React, { memo } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { Modal } from 'Components/Modal';
import { Button } from 'Components/Button';
import classes from './deleteFormModal.module.css';
interface Props {
id: number;
isOpen: boolean;
modalCloseClick: (e: any) => void;
onDelete: (id: number) => void;
}
const DeleteFormModal = ({ id, isOpen, modalCloseClick, onDelete }: Props) => (
<div>
<Modal
id={id && id.toString()}
classes={classes}
title="Delete Contract Form?"
isOpen={isOpen}
modalHeader
footerButtons={
<Button
className={`${classes.delete}`}
id={id && id.toString()}
onClick={(e) => {
e.preventDefault();
onDelete(id);
}}
>
Delete
</Button>
}
modalFooter
closeButtonText="Cancel"
dataBsBackdrop="static"
dataBsKeyboard="false"
modalCloseClick={modalCloseClick}
>
<div className={classes.deleteModalCopy}>
<p>Are you sure you want to delete this form?</p>
</div>
</Modal>
</div>
);
const DeleteFormModalMemo = memo(DeleteFormModal, areEqual);
export { DeleteFormModalMemo as DeleteFormModal };
@@ -0,0 +1,86 @@
.modalContent {
padding: 1.3em 1.5em;
box-shadow: 0px 24px 44px rgba(119, 113, 133, 0.2);
border-radius: 5px;
}
.modalDialog {
max-width: 648px;
}
.modalHeader {
padding: 0 0 0.5em 0;
border-color: #e8e7ed;
height: 40px;
}
.modalTitle {
font-family: IBM Plex Sans;
font-style: normal;
font-weight: 600;
font-size: 18px;
text-transform: capitalize;
line-height: 24px;
color: #5b476b;
text-align: center;
flex: 1;
}
.modalBody {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 1.3rem 0 1.1rem 0;
}
.modalBody {
font-family: IBM Plex Sans;
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #000000;
}
.deleteModalHeader {
width: 100%;
max-width: 343px;
border-bottom: 1px solid #d2cfda;
}
.deleteModalCopy {
width: 350px;
text-transform: capitalize;
font-weight: 500;
}
.modalFooter {
padding: 1rem 0 0 0;
border: none;
flex-direction: row-reverse;
justify-content: center;
}
.modalFooter button {
font-family: IBM Plex Sans;
font-style: normal;
font-weight: 600;
font-size: 16px;
line-height: 24px;
color: #5b476b;
background-color: #ffffff;
border: 1px solid #5b476b;
border-radius: 5px;
width: 151px;
height: 44px;
}
.modalFooter .closeButtonClass:hover,
.modalFooter .closeButtonClass:focus {
background-color: #f3f3f6;
border-color: #d2cfda;
color: #777185;
}
.modalFooter .delete {
border-color: #e82828 !important;
color: #e82828 !important;
}
.modalFooter .delete:hover,
.modalFooter .delete:focus {
background-color: #e82828 !important;
color: #ffffff !important;
}
@@ -0,0 +1 @@
export { DeleteFormModal } from './DeleteFormModal';
@@ -0,0 +1,46 @@
import React, { memo } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { CheckedMarkSvg } from 'Components/Icons/CheckedMark';
import { Button } from 'Components/Button';
import classes from './deleteToast.module.css';
export interface Props {
isDisplayed: boolean;
message: string;
closeToast: (e: any) => void;
}
const DeleteToast = ({ isDisplayed = false, message, closeToast }: Props) => (
<div
className={`toast fade d-flex align-items-center position-fixed border-0 bottom-0 ${
isDisplayed ? 'show' : 'hide'
} ${classes.toastBase} ${classes.toastSuccess}`}
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div className={`toast-body ${classes['toast-body-override']} ${classes.toastText}`}>
{message}
<span className={`${classes.toastIcon}`}>
<CheckedMarkSvg />
</span>
</div>
<div className={classes.toastCloseButtonContainer}>
<Button
type="button"
className={`btn-close ${classes.toastCloseButton}`}
data-bs-dismiss="toast"
aria-label="Close"
onClick={closeToast}
/>
</div>
</div>
);
DeleteToast.defaultProps = {};
const DeleteToastMemo = memo(DeleteToast, areEqual);
export { DeleteToastMemo as DeleteToast };
@@ -0,0 +1,77 @@
.toastBase {
height: 40px !important;
width: calc(100% - (315px + 1.5rem)) !important;
z-index: 1000;
box-shadow: none;
border-radius: 0;
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
padding-top: 1rem !important;
padding-bottom: 1rem !important;
transition: opacity 0.15s linear;
left: calc(290px + 0.75rem);
text-align: center;
}
.toast-body-override {
width: 95%;
padding: unset ip !important;
}
.toastCloseButtonContainer {
display: flex;
justify-content: flex-end;
width: 10%;
}
.toastCloseButton {
border-radius: 50% !important;
color: #5b476b !important;
background-color: #fff !important;
padding: 0.25rem !important;
opacity: unset !important;
/* This is the actual svg pulled from bootstrap. not the 0.5em beside center on the 2nd line. This is what has changed */
background: transparent
url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e")
center/0.5em auto no-repeat;
}
.toastIcon {
padding-left: 10px;
}
.toastCloseButton:hover {
color: #000;
text-decoration: none;
opacity: unset !important;
}
.toastText {
font-family: IBM Plex Sans !important;
font-style: normal !important;
font-weight: 600 !important;
font-size: 16px !important;
line-height: 24px !important;
color: #5b476b !important;
}
.toastSuccess {
background-color: #dcf5f0;
border-bottom: 1px solid #40c9ae !important;
}
.toastWarning {
background-color: #fff0f0;
border-bottom: 1px solid #e82828;
}
.toastCloseIcon {
position: absolute;
right: 22px;
}
@media (max-width: 768px) {
.toastBase {
width: 100%;
/* height: auto; */
padding: 12px 66px 12px 22px;
}
.toastText {
font-size: 14px;
}
}
@@ -0,0 +1 @@
export { DeleteToast } from './DeleteToast';
@@ -1,2 +1,4 @@
export { ContractForms } from './ContractForms';
export { FormsList } from './FormsList';
export { DeleteFormModal } from './DeleteFormModal';
export { DeleteToast } from './DeleteToast';
@@ -1,15 +1,29 @@
import React, { memo, useEffect, useCallback } from 'react';
import React, { memo, useEffect, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { areEqual } from 'Utils/equalityChecks';
import { contractFormsSelector, fetchingContractFormsSelector, companyIdSelector } from 'Containers/Forms/selectors';
import {
contractFormsSelector,
fetchingContractFormsSelector,
companyIdSelector,
deletingContractFormSelector,
contractFormDeletedSelector,
} from 'Containers/Forms/selectors';
import { ContractForms } from 'Components/Forms';
import { listCompanyContractForms } from 'Containers/Forms/actions';
import { DeleteFormModal, DeleteToast } from 'Components/Forms/FormTabs';
import { listCompanyContractForms, deleteContractForm, setDeletedFormId } from '../../actions';
const ContractFormsContainer = () => {
const dispatch = useDispatch();
const contractForms = useSelector(contractFormsSelector, areEqual);
const fetching = useSelector(fetchingContractFormsSelector, areEqual);
const companyId = useSelector(companyIdSelector, areEqual);
const deleting = useSelector(deletingContractFormSelector, areEqual);
const deletedId = useSelector(contractFormDeletedSelector, areEqual);
const [deleteModal, setDeleteModal] = useState({
isOpen: false,
id: null,
});
const [showDeletedToast, setShowDeletedToast] = useState(false);
const getContractForms = useCallback(() => {
if (companyId) {
@@ -17,18 +31,70 @@ const ContractFormsContainer = () => {
}
}, [dispatch, companyId]);
const deleteButtonClick = useCallback((id: number) => {
setDeleteModal({
isOpen: true,
id,
});
}, []);
const handleDelete = useCallback(
(id: number) => {
dispatch(deleteContractForm(id));
setDeleteModal({
isOpen: false,
id: null,
});
},
[dispatch]
);
const closeToast = useCallback((e: any) => {
e.preventDefault();
setShowDeletedToast(false);
}, []);
useEffect(() => {
if (deletedId) {
dispatch(listCompanyContractForms(companyId));
setShowDeletedToast(true);
setTimeout(() => setShowDeletedToast(false), 1500);
dispatch(setDeletedFormId(null));
}
}, [deletedId]);
useEffect(() => {
getContractForms();
}, [getContractForms]);
return (
<>
<ContractForms
forms={contractForms}
fetching={fetching}
onAdd={console.log}
onDelete={console.log}
onClickRow={console.log}
onAdd={() => {}}
onDelete={deleteButtonClick}
onClickRow={() => {}}
// deleting={deleting}
// deletedId={deletedId}
/>
<DeleteFormModal
id={deleteModal.id as number}
isOpen={deleteModal.isOpen}
modalCloseClick={() =>
setDeleteModal({
isOpen: false,
id: null,
})
}
onDelete={handleDelete}
/>
{deletedId && <DeleteToast isDisplayed={showDeletedToast} closeToast={closeToast} message="Form Deleted" />}
</>
);
};
+20
View File
@@ -4,10 +4,14 @@ import { FormTemplateResponse } from 'Containers/Forms/Models';
export const CONTRACT_FORMS = 'CONTRACT_FORMS';
export const FETCHING_CONTRACT_FORMS = 'FETCHING_CONTRACT_FORMS';
export const DELETING_CONTRACT_FORM = 'DELETING_CONTRACT_FORM';
export const CONTRACT_FORM_DELETED = 'CONTRACT_FORM_DELETED';
interface FormsActionTypes {
CONTRACT_FORMS: FormTemplateResponse;
FETCHING_CONTRACT_FORMS: boolean;
DELETING_CONTRACT_FORM: boolean;
CONTRACT_FORM_DELETED: number;
}
interface MessageAction {
@@ -31,3 +35,19 @@ export const listCompanyContractForms = (companyId: number) => async (dispatch:
payload: response,
});
};
// Thunk to delete a contract form by id
export const deleteContractForm = (contractFormId: number) => async (dispatch: any) => {
dispatch({ type: DELETING_CONTRACT_FORM, payload: true });
try {
await handleApiRequest(dispatch, Api.delete(`/contract-forms/${contractFormId}`), '', DELETING_CONTRACT_FORM);
dispatch({ type: CONTRACT_FORM_DELETED, payload: contractFormId });
} catch (error) {
// handle error if needed
dispatch({ type: DELETING_CONTRACT_FORM, payload: false });
}
};
export const setDeletedFormId = (value: number | null) => async (dispatch: any) => {
dispatch({ type: CONTRACT_FORM_DELETED, payload: value });
};
+21 -1
View File
@@ -1,8 +1,16 @@
import { CONTRACT_FORMS, FETCHING_CONTRACT_FORMS, SetFormsActionTypes } from './actions';
import {
CONTRACT_FORMS,
FETCHING_CONTRACT_FORMS,
SetFormsActionTypes,
DELETING_CONTRACT_FORM,
CONTRACT_FORM_DELETED,
} from './actions';
const initialState = {
data: [],
fetchingContractForms: false,
deletingContractForm: false,
contractFormDeleted: null,
};
export const formsReducer = (state = initialState, action: SetFormsActionTypes) => {
@@ -19,6 +27,18 @@ export const formsReducer = (state = initialState, action: SetFormsActionTypes)
fetchingContractForms: action.payload,
};
}
case DELETING_CONTRACT_FORM: {
return {
...state,
deletingContractForm: action.payload,
};
}
case CONTRACT_FORM_DELETED: {
return {
...state,
contractFormDeleted: action.payload,
};
}
default:
return state;
}
+2
View File
@@ -2,3 +2,5 @@
export const contractFormsSelector = ({ forms }: any) => forms?.data || [];
export const fetchingContractFormsSelector = ({ forms }: any) => forms?.fetchingContractForms || false;
export { firstCompanyIdSelector as companyIdSelector } from '../Projects/selectors';
export const deletingContractFormSelector = ({ forms }: any) => forms?.deletingContractForm || false;
export const contractFormDeletedSelector = ({ forms }: any) => forms?.contractFormDeleted || null;