Refactored job line edit modal to use redux so that it can be an independent form. Created general modals reducer to manage all modals.

This commit is contained in:
Patrick Fic
2020-02-24 12:18:54 -08:00
parent 31e0a1f081
commit c21f3c0098
13 changed files with 383 additions and 190 deletions

View File

@@ -15,107 +15,168 @@ export default ({
}) => {
const { t } = useTranslation();
//TODO Add
return (
<Row type='flex' justify='space-around' align='middle'>
<Row type="flex" justify="space-around" align="middle">
{logo ? (
<Col span={4}>
<img alt='Shop Logo' src={logo} style={{ height: "40px" }} />
<Col span={3}>
<img alt="Shop Logo" src={logo} style={{ height: "40px" }} />
</Col>
) : null}
<Col span={14}>
<Menu
theme='dark'
className='header'
selectedKeys={selectedNavItem}
mode='horizontal'
onClick={handleMenuClick}>
<Menu.Item key='home'>
<Link to='/manage'>
<Icon type='home' />
{t("menus.header.home")}
</Link>
</Menu.Item>
<Menu.SubMenu title={t("menus.header.jobs")}>
<Menu.Item key='schedule'>
<Link to='/manage/schedule'>
<Icon type='calendar' />
{t("menus.header.schedule")}
</Link>
</Menu.Item>
<Menu.Item key='activejobs'>
<Link to='/manage/jobs'>{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item key='availablejobs'>
<Link to='/manage/available'>
{t("menus.header.availablejobs")}
</Link>
</Menu.Item>
</Menu.SubMenu>
{landingHeader ? (
<Menu
theme="dark"
className="header"
selectedKeys={selectedNavItem}
mode="horizontal"
onClick={handleMenuClick}
>
<ManageSignInButton />
<Menu.SubMenu title={t("menus.header.customers")}>
<Menu.Item key='owners'>
<Link to='/manage/owners'>
<Icon type='team' />
{t("menus.header.owners")}
</Link>
</Menu.Item>
<Menu.Item key='vehicles'>
<Link to='/manage/vehicles'>
<Icon type='car' />
{t("menus.header.vehicles")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.shop")}>
<Menu.Item key='shop'>
<Link to='/manage/shop'>{t("menus.header.shop_config")}</Link>
</Menu.Item>
<Menu.Item key='shop-vendors'>
<Link to='/manage/shop/vendors'>
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<div>
<Avatar
size='medium'
alt='Avatar'
src={currentUser.photoURL ? currentUser.photoURL : UserImage}
style={{ margin: "10px" }}
/>
{currentUser.displayName || t("general.labels.unknown")}
</div>
}>
<Menu.Item onClick={signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item>
<Link to='/manage/profile'>{t("menus.currentuser.profile")}</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon type='global' />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}>
<Menu.Item actiontype='lang-select' key='en_us'>
{t("general.languages.english")}
<div>
<Avatar
size="medium"
alt="Avatar"
src={
currentUser.photoURL ? currentUser.photoURL : UserImage
}
style={{ margin: "10px" }}
/>
{currentUser.displayName || t("general.labels.unknown")}
</div>
}
>
<Menu.Item onClick={signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item actiontype='lang-select' key='fr'>
{t("general.languages.french")}
<Menu.Item>
<Link to="/manage/profile">
{t("menus.currentuser.profile")}
</Link>
</Menu.Item>
<Menu.Item actiontype='lang-select' key='es'>
{t("general.languages.spanish")}
<Menu.SubMenu
title={
<span>
<Icon type="global" />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}
>
<Menu.Item actiontype="lang-select" key="en_us">
{t("general.languages.english")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="fr">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es">
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
) : (
<Menu
theme="dark"
className="header"
selectedKeys={selectedNavItem}
mode="horizontal"
onClick={handleMenuClick}
>
<Menu.Item key="home">
<Link to="/manage">
<Icon type="home" />
{t("menus.header.home")}
</Link>
</Menu.Item>
<Menu.SubMenu title={t("menus.header.jobs")}>
<Menu.Item key="schedule">
<Link to="/manage/schedule">
<Icon type="calendar" />
{t("menus.header.schedule")}
</Link>
</Menu.Item>
<Menu.Item key="activejobs">
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item key="availablejobs">
<Link to="/manage/available">
{t("menus.header.availablejobs")}
</Link>
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
<Menu.SubMenu title={t("menus.header.customers")}>
<Menu.Item key="owners">
<Link to="/manage/owners">
<Icon type="team" />
{t("menus.header.owners")}
</Link>
</Menu.Item>
<Menu.Item key="vehicles">
<Link to="/manage/vehicles">
<Icon type="car" />
{t("menus.header.vehicles")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.shop")}>
<Menu.Item key="shop">
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
</Menu.Item>
<Menu.Item key="shop-vendors">
<Link to="/manage/shop/vendors">
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<div>
<Avatar
size="medium"
alt="Avatar"
src={
currentUser.photoURL ? currentUser.photoURL : UserImage
}
style={{ margin: "10px" }}
/>
{currentUser.displayName || t("general.labels.unknown")}
</div>
}
>
<Menu.Item onClick={signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item>
<Link to="/manage/profile">
{t("menus.currentuser.profile")}
</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon type="global" />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}
>
<Menu.Item actiontype="lang-select" key="en_us">
{t("general.languages.english")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="fr">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es">
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
)}
</Col>
<Col span={4}>{!landingHeader ? null : <ManageSignInButton />}</Col>
</Row>
);
};

View File

@@ -1,14 +1,13 @@
import { Button, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import { Link } from "react-router-dom";
//import EditableCell from "./job-lines-cell.component";
import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
import JobLineUpsertModalContainer from "../job-lines-upsert-modal/job-lines-upsert-modal.container";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
export default function JobLinesComponent({
loading,
refetch,
@@ -18,8 +17,7 @@ export default function JobLinesComponent({
setSelectedLines,
partsOrderModalVisible,
jobId,
editLineModalVisible,
lineToEdit
setJobLineEditContext
}) {
const [state, setState] = useState({
sortedInfo: {}
@@ -145,9 +143,8 @@ export default function JobLinesComponent({
{record.allocations && record.allocations.length > 0
? record.allocations.map(item => (
<div
key={
item.id
}>{`${item.employee.first_name} ${item.employee.last_name} (${item.hours})`}</div>
key={item.id}
>{`${item.employee.first_name} ${item.employee.last_name} (${item.hours})`}</div>
))
: null}
<AllocationsAssignmentContainer
@@ -167,9 +164,14 @@ export default function JobLinesComponent({
<span>
<Button
onClick={() => {
lineToEdit[1](record);
editLineModalVisible[1](true);
}}>
setJobLineEditContext({
actions: { refetch: refetch },
context: record
});
//lineToEdit[1](record);
//editLineModalVisible[1](true);
}}
>
{t("general.actions.edit")}
</Button>
</span>
@@ -201,14 +203,6 @@ export default function JobLinesComponent({
jobId={jobId}
/>
<JobLineUpsertModalContainer
jobId={jobId}
visible={editLineModalVisible[0]}
changeVisibility={editLineModalVisible[1]}
refetch={refetch}
existingLine={lineToEdit[0]}
/>
<Table
title={() => {
return (
@@ -222,7 +216,8 @@ export default function JobLinesComponent({
/>
<Button
disabled={selectedLines.length > 0 ? false : true}
onClick={() => setPartsModalVisible(true)}>
onClick={() => setPartsModalVisible(true)}
>
{t("parts.actions.order")}
</Button>
<AllocationsBulkAssignmentContainer
@@ -234,7 +229,7 @@ export default function JobLinesComponent({
}}
{...formItemLayout}
loading={loading}
size='small'
size="small"
expandedRowRender={record => (
<div style={{ margin: 0 }}>
<strong>{t("parts_orders.labels.orderhistory")}</strong>
@@ -252,11 +247,14 @@ export default function JobLinesComponent({
pagination={{ position: "top", defaultPageSize: 25 }}
rowSelection={{
// selectedRowKeys: selectedLines,
onSelectAll: (selected, selectedRows, changeRows) => {
setSelectedLines(selectedRows);
},
onSelect: (record, selected, selectedRows, nativeEvent) =>
setSelectedLines(selectedRows)
}}
columns={columns.map(item => ({ ...item }))}
rowKey='id'
rowKey="id"
dataSource={jobLines}
onChange={handleTableChange}
/>

View File

@@ -5,9 +5,16 @@ import { GET_JOB_LINES_BY_PK } from "../../graphql/jobs-lines.queries";
import AlertComponent from "../alert/alert.component";
import JobLinesComponent from "./job-lines.component";
//export default Form.create({ name: "JobsDetailJobLines" })(
export default function JobLinesContainer({ jobId }) {
import { connect } from "react-redux";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapDispatchToProps = dispatch => ({
setJobLineEditContext: context =>
dispatch(setModalContext({ context: context, modal: "jobLineEdit" }))
});
export default connect(
null,
mapDispatchToProps
)(function JobLinesContainer({ jobId, setJobLineEditContext }) {
const { loading, error, data, refetch } = useQuery(GET_JOB_LINES_BY_PK, {
variables: { id: jobId },
fetchPolicy: "network-only"
@@ -16,10 +23,8 @@ export default function JobLinesContainer({ jobId }) {
const [searchText, setSearchText] = useState("");
const [selectedLines, setSelectedLines] = useState([]);
const partsOrderModalVisible = useState(false);
const editLineModalVisible = useState(false);
const lineToEdit = useState({});
if (error) return <AlertComponent message={error.message} type='error' />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<JobLinesComponent
@@ -61,9 +66,7 @@ export default function JobLinesContainer({ jobId }) {
setSelectedLines={setSelectedLines}
partsOrderModalVisible={partsOrderModalVisible}
jobId={jobId}
editLineModalVisible={editLineModalVisible}
lineToEdit={lineToEdit}
setJobLineEditContext={setJobLineEditContext}
/>
);
}
//);
});

View File

@@ -1,31 +1,33 @@
import { Input, Modal } from "antd";
import { Modal, Form } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function JobLinesUpsertModalComponent({
visible,
changeVisibility,
lineState,
setLineState,
updateExistingLine,
insertNewLine
jobLine,
handleOk,
handleCancel,
handleSubmit,
form
}) {
const { t } = useTranslation();
const handleChange = e => {
setLineState({ ...lineState, [e.target.name]: e.target.value });
};
//const { getFieldDecorator, isFieldsTouched, resetFields } = form;
console.log("jobLine", jobLine);
return (
<Modal
title={lineState.id ? t("joblines.label.edit") : t("joblines.label.new")}
title={
jobLine && jobLine.id
? t("joblines.labels.edit")
: t("joblines.labels.new")
}
visible={visible}
okText={t("general.labels.save")}
onOk={() => {
lineState.id ? updateExistingLine() : insertNewLine();
}}
onCancel={() => {
changeVisibility(false);
}}>
<Input.TextArea rows={8} value={lineState.text} onChange={handleChange} />
onOk={handleOk}
onCancel={handleCancel}
>
<Form onSubmit={handleSubmit} autoComplete={"off"}>
{JSON.stringify(jobLine)}
</Form>
</Modal>
);
}

View File

@@ -1,71 +1,102 @@
import { notification } from "antd";
import React, { useEffect, useState } from "react";
import { Form, notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_NEW_JOB_LINE,
UPDATE_JOB_LINE
} from "../../graphql/jobs-lines.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
export default function JobLinesUpsertModalContainer({
jobId,
visible,
changeVisibility,
refetch,
existingLine
const mapStateToProps = createStructuredSelector({
jobLineEditModal: selectJobLineEditModal
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit"))
});
function JobLinesUpsertModalContainer({
jobLineEditModal,
toggleModalVisible,
form
}) {
const { t } = useTranslation();
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
const [lineState, setLineState] = useState({});
const handleSubmit = e => {
e.preventDefault();
useEffect(() => {
//Required to prevent infinite looping.
if (existingLine) {
setLineState(existingLine);
}
}, [existingLine]);
const insertNewLine = () => {
insertJobLine({
variables: {
lineInput: [{ ...lineState, jobid: jobId }]
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("jobs.errors.validationtitle"),
description: t("jobs.errors.validation")
});
}
if (!err) {
if (true) {
insertJobLine({
variables: {
//lineInput: [{ ...lineState, jobid: jobId }]
}
}).then(r => {
if (jobLineEditModal.actions.refetch)
jobLineEditModal.actions.refetch();
toggleModalVisible();
notification["success"]({
message: t("joblines.successes.create")
});
});
}
if (false) {
//Required, otherwise unable to spread in new note prop.
//delete lineState.__typename;
updateJobLine({
variables: {
//lineId: lineState.id,
//line: lineState
}
}).then(r => {
notification["success"]({
message: t("joblines.successes.updated")
});
});
if (jobLineEditModal.actions.refetch)
jobLineEditModal.actions.refetch();
toggleModalVisible();
}
}
}).then(r => {
refetch();
changeVisibility(!visible);
notification["success"]({
message: t("joblines.successes.create")
});
});
};
const updateExistingLine = () => {
//Required, otherwise unable to spread in new note prop.
delete lineState.__typename;
updateJobLine({
variables: {
lineId: lineState.id,
line: lineState
}
}).then(r => {
notification["success"]({
message: t("joblines.successes.updated")
});
});
refetch();
changeVisibility(!visible);
const handleOk = () => {
//lineState.id ? updateExistingLine() : insertNewLine();
};
const handleCancel = () => {
toggleModalVisible();
};
return (
<JobLinesUpdsertModal
visible={visible}
changeVisibility={changeVisibility}
updateExistingLine={updateExistingLine}
insertNewLine={insertNewLine}
lineState={lineState}
setLineState={setLineState}
visible={jobLineEditModal.visible}
jobLine={jobLineEditModal.context}
handleSubmit={handleSubmit}
handleOk={handleOk}
handleCancel={handleCancel}
form={form}
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "JobsDetailPageContainer" })(JobLinesUpsertModalContainer)
);

View File

@@ -1,7 +1,12 @@
import { Form, Icon, Tabs } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import { FaHardHat, FaInfo, FaRegStickyNote, FaShieldAlt } from "react-icons/fa";
import {
FaHardHat,
FaInfo,
FaRegStickyNote,
FaShieldAlt
} from "react-icons/fa";
import ResetForm from "../../components/form-items-formatted/reset-form-item.component";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
import JobsDetailClaims from "../../components/jobs-detail-claims/jobs-detail-claims.component";
@@ -13,6 +18,7 @@ import JobsDocumentsContainer from "../../components/jobs-documents/jobs-documen
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
import JobDetailFormContext from "./jobs-detail.page.context";
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
export default function JobsDetailPage({
job,
@@ -46,6 +52,8 @@ export default function JobsDetailPage({
refetch={refetch}
/>
<JobLineUpsertModalContainer />
<Form onSubmit={handleSubmit} {...formItemLayout} autoComplete={"off"}>
<JobsDetailHeader
job={job}

View File

@@ -0,0 +1,12 @@
import ModalsActionTypes from "./modals.types";
export const toggleModalVisible = modalName => ({
type: ModalsActionTypes.TOGGLE_MODAL_VISIBLE,
payload: modalName
});
//Modal Context: {context (context object), modal(name of modal)}
export const setModalContext = modalContext => ({
type: ModalsActionTypes.SET_MODAL_CONTEXT,
payload: modalContext
});

View File

@@ -0,0 +1,37 @@
import ModalsActionTypes from "./modals.types";
const INITIAL_STATE = {
jobLineEdit: {
visible: false,
context: null,
actions: {
refetch: null
}
}
};
const modalsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ModalsActionTypes.TOGGLE_MODAL_VISIBLE:
return {
...state,
[action.payload]: {
...state[action.payload],
visible: !state[action.payload].visible
}
};
case ModalsActionTypes.SET_MODAL_CONTEXT:
return {
...state,
[action.payload.modal]: {
...state[action.payload.modal],
...action.payload.context,
visible: true
}
};
default:
return state;
}
};
export default modalsReducer;

View File

@@ -0,0 +1,24 @@
import { all } from "redux-saga/effects";
// export function* onSendEmail() {
// yield takeLatest(EmailActionTypes.SEND_EMAIL, sendEmail);
// }
// export function* sendEmail(payload) {
// try {
// console.log("Sending thta email", payload);
// axios.post("/sendemail", payload).then(response => {
// console.log(JSON.stringify(response));
// put(sendEmailSuccess());
// });
// } catch (error) {
// console.log("Error in sendEmail saga.");
// yield put(sendEmailFailure(error.message));
// }
// }
export function* modalsSagas() {
yield all([
//call(onSendEmail),
]);
}

View File

@@ -0,0 +1,9 @@
import { createSelector } from "reselect";
const selectModals = state => state.modals;
export const selectJobLineEditModal = createSelector(
[selectModals],
modals => modals.jobLineEdit
);

View File

@@ -0,0 +1,5 @@
const ModalActionTypes = {
TOGGLE_MODAL_VISIBLE: "TOGGLE_MODAL_VISIBLE",
SET_MODAL_CONTEXT: "SET_JOBLINEEDIT_CONTEXT"
};
export default ModalActionTypes;

View File

@@ -5,18 +5,19 @@ import storage from "redux-persist/lib/storage";
import userReducer from "./user/user.reducer";
import messagingReducer from "./messaging/messaging.reducer";
import emailReducer from "./email/email.reducer";
import modalsReducer from './modals/modals.reducer'
const persistConfig = {
key: "root",
storage,
//whitelist: ["user"]
blacklist: ["user", "email", "messaging"]
blacklist: ["user", "email", "messaging", "modals"]
};
const rootReducer = combineReducers({
user: userReducer,
messaging: messagingReducer,
email: emailReducer
email: emailReducer,
modals: modalsReducer
});
export default persistReducer(persistConfig, rootReducer);

View File

@@ -3,6 +3,8 @@ import { all, call } from "redux-saga/effects";
import { userSagas } from "./user/user.sagas";
import { messagingSagas } from "./messaging/messaging.sagas";
import { emailSagas } from "./email/email.sagas";
import { modalsSagas } from "./modals/modals.sagas";
export default function* rootSaga() {
yield all([call(userSagas), call(messagingSagas), call(emailSagas)]);
yield all([call(userSagas), call(messagingSagas), call(emailSagas),
call(modalsSagas)]);
}