I'm using material-ui to list group of violations to the user to select one or multiple violations, then during the selection i extract from each violation it's id and update the state so as a result i'll have an array of ids to send it to backend
here is my code
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import get from 'lodash/get';
// Material-UI
import MaterialTable, { MTableToolbar } from 'material-table';
import { withStyles } from '#material-ui/core/styles';
import Grid from '#material-ui/core/Grid';
import Paper from '#material-ui/core/Paper';
import Button from '#material-ui/core/Button';
import SaveIcon from '#material-ui/icons/Save';
import CancelIcon from '#material-ui/icons/Cancel';
// Forms
import Entity from '~/Components/Entity';
import { BaseFormComponent, Form, FormContainer, FormItem } from '~/Components/FormComponent';
import { LabelAndValue, LookupString } from '~/Components/FormComponent/Controls';
import {
isRequired,
minNumbers,
maxNumbers,
onlyNumbers,
noEnglish
} from '~/Services/Validators';
import Navigate from '~/Services/Navigate';
import Notifications from '~/Services/Notifications';
import Message from '~/Components/Message';
import Strings from '~/Services/Strings';
import styles from './styles';
#withStyles(styles)
class violationEditorScreen extends BaseFormComponent {
constructor(props) {
super(props);
this.initState({
error: false,
errorMsg: '',
requestId: null,
complaintId: null,
penalityTypeId: null,
violations: [],
inistitutionsData: [],
selectedViolationsId: [],
form: {
id: {
type: this.types.number,
value: 0,
},
districtId: {
type: this.types.number,
value: 1,
UIOnly: true
},
cityId: {
type: this.types.number,
value: 1,
UIOnly: true
},
institutionTypeId: {
type: this.types.number,
value: 2,
UIOnly: true
},
complaintTitle: {
type: this.types.string,
validators: [ isRequired(), noEnglish() ],
},
complaintDescription: {
type: this.types.string,
validators: [ isRequired(), noEnglish() ],
},
institutionId: {
type: this.types.number,
validators: [ isRequired() ],
},
}
});
}
componentDidMount() {
super.componentDidMount();
// const id = get(this, 'props.match.params.id', null);
// if (id) {
// this.addFormFields({
// });
// }
this.getInstitutionsList();
}
getInstitutionsList() {
const { form } = this.state;
this.getInstitutionsEntity.get({
cityId: form.cityId.value,
districtId: form.districtId.value,
institutionTypeId: form.institutionTypeId.value
});
}
// On Institution Change
onHandleInstitutionChange(institutionId) {
this.getRequestIdEntity.post({ institutionId });
}
getRequestIdEntityPosted(data) {
const requestId = data.requestId;
const complaintId = data.id;
this.setState({
requestId,
complaintId
}, () => {
this.getViolationsEntity.get({
complaintId
});
});
}
onViolationsEntityReceived(data) {
const violations = [];
if(data && data.length > 0) {
data.map(item => {
violations.push({ ...item });
});
this.setState({ violations });
}
this.setState({ violations });
}
onInstitutionEntityReceived(data) {
if(data && data.licensingInstitutionsModel && data.licensingInstitutionsModel.length > 0) {
const arr = [];
data.licensingInstitutionsModel.map(item => {
arr.push({
id: item.institutionId,
nameAr: item.fullName
});
});
this.setState({ inistitutionsData: arr });
}
}
onEntityPosted(data) {
const requestId = data.requestId;
Notifications.notify('success', Strings.complaintHasBeenSuccessfullyPublished);
this.getViolationsEntity.post({ requestId });
}
onSubmit() {
const id = get(this, 'props.match.params.id', null);
const { selectedViolationsId, requestId } = this.state;
if (this.isFormValid) {
if(selectedViolationsId.length === 0) {
this.setState({
error: true,
errorMsg: Strings.addAtLeastOneViolation
});
}else {
const payload = {
...this.formValues,
selectedViolationsId,
requestId,
id: id ? id : 0
};
this.entity.post(payload);
}
} else {
this.showFormErrors();
}
}
handleSelectedRows(rows) {
const selectedViolationsId = [];
const penalityTypeIds = [];
if(rows.length > 0) {
rows.map(row => {
selectedViolationsId.push(row.id);
penalityTypeIds.push(row.penaltyTypeId);
});
this.setState({ selectedViolationsId }, () => {
if(penalityTypeIds.length > 0) {
const validators= [
isRequired(),
minNumbers(1),
onlyNumbers()
];
const penalityTypeId = penalityTypeIds.sort((a, b) => {
if(a > b) return -1;
if(b > a) return 1;
})[0];
if(penalityTypeId === 1 || penalityTypeId === 2) {
validators.push(maxNumbers(30));
} else {
validators.push(maxNumbers(60));
}
this.addFormFields({
settlementPeriodInDays: {
type: this.types.number,
validators
},
});
this.setState({ penalityTypeId });
} else {
this.setState({ penalityTypeId: null });
}
});
} else {
this.setState({
selectedViolationsId: [],
penalityTypeId: null
});
}
}
get localization() {
return {
header: {
actions: Strings.listActionsLabel,
},
body: {
emptyDataSourceMessage: Strings.listEmptyLabel,
},
pagination: {
labelRowsPerPage: Strings.rowsPerPageLabel,
labelDisplayedRows: `{from}-{to} ${Strings.fromText} {count}`,
},
toolbar: {
nRowsSelected: `${Strings.nSelected} {0} ${Strings.selectedViolations}`
}
};
}
get options() {
return {
actionsColumnIndex: -1,
pageSize: 10,
selection: true,
filtering: true,
columnsButton: true,
maxBodyHeight: 600,
pageSizeOptions: [ 5, 10 ] ,
doubleHorizontalScroll: true,
rowStyle: row => {
if ( row.tableData.id % 2 ) {
return { backgroundColor: '#f2f2f2' };
}
}
};
}
get columns() {
return [
{ title: Strings.violationReferenceNumber, field: 'referenceNumber', cellStyle: { width: 120 } },
{ title: Strings.violationDescription, field: 'description' },
];
}
get components() {
const { classes } = this.props;
return {
Toolbar: props => (
<div className={classes.toolbar}>
<MTableToolbar {...props} />
</div>
),
};
}
render() {
const { form, error, errorMsg, inistitutionsData, violations, penalityTypeId } = this.state;
const { classes } = this.props;
const {
TextField,
LookupSelectField,
SelectAutocompleteField,
} = this;
return (
<React.Fragment>
<Entity
storeId={'Supervision-Complaints-Editor'}
entityRef={ref => { this.entity = ref; }}
onEntityReceived={data => this.onEntityReceived(data)}
onEntityPosted={data => this.onEntityPosted(data)}
onEntityPostedError={data => this.onEntityPostedError(data)}
render={store => (
<React.Fragment>
<If condition={error}>
<Grid item xs={12}>
<Message variant={'error'} text={errorMsg} />
</Grid>
</If>
<Form loading={store.loading}>
<Grid container spacing={24}>
<Grid item xs={9}>
<Paper elevation={1} className={classes.box1}>
<fieldset className={classes.fieldSet}>
<legend>{Strings.complaintDetails}</legend>
<FormContainer>
<FormItem lg={4}>
<LookupSelectField
name={'districtId'}
label={Strings.selectDistrictToSearch}
lookup={'Districts'}
onChange={() => this.getInstitutionsList()}
autocomplete
/>
</FormItem>
<FormItem lg={4}>
<LookupSelectField
name={'cityId'}
label={Strings.selectCityToSearch}
lookup={`City/LookupItemsByParentId/${form.districtId.value}`}
onChange={() => this.getInstitutionsList()}
autocomplete
/>
</FormItem>
<FormItem lg={4}>
<LookupSelectField
name={'institutionTypeId'}
label={Strings.selectInstitutionTypeToSearch}
lookup={'InstitutionTypes'}
onChange={() => this.getInstitutionsList()}
/>
</FormItem>
<FormItem lg={4}>
<div className={classnames(classes.placeholder, {})}>
<SelectAutocompleteField
name={'institutionId'}
label={Strings.assignmentInstitutionName}
emptyString={Strings.searchByNameAndLicense}
data={inistitutionsData}
onChange={field => this.onHandleInstitutionChange(field.value)}
/>
</div>
</FormItem>
<FormItem lg={4}>
<TextField
name={'complaintTitle'}
label={Strings.complaintTitle}
setBorder={false}
/>
</FormItem>
<If condition={penalityTypeId}>
<FormItem lg={4}>
<TextField
name={'settlementPeriodInDays'}
label={Strings.insertSettlementPeriodInDays}
setBorder={false}
/>
</FormItem>
</If>
<FormItem fullWidth>
<TextField
multiline
name={'complaintDescription'}
label={Strings.complaintDescription}
/>
</FormItem>
</FormContainer>
</fieldset>
</Paper>
<Paper elevation={1} className={classes.box}>
<fieldset className={classes.fieldSet}>
<legend>{Strings.complaintAttachments}</legend>
<FormContainer>
</FormContainer>
</fieldset>
{/* Attachment Here */}
</Paper>
<If condition={violations.length > 0}>
<Paper elevation={1} className={classes.box}>
<MaterialTable
title={Strings.complaintsAddViolationList}
data={violations}
options={this.options}
localization={this.localization}
columns={this.columns}
components={this.components}
onSelectionChange={rows => this.handleSelectedRows(rows)}
/>
</Paper>
</If>
</Grid>
<Grid item xs={3}>
{/* =========== Sidebar ============= */}
<If condition={penalityTypeId}>
<Paper elevation={1} className={classes.box}>
<FormItem fullWidth style={{ marginBottom: 10 }}>
<LabelAndValue
label={Strings.earnedPenality}
className={classes.deservedPenality}
value={(<LookupString
lookup={'PenaltyType'}
value={penalityTypeId}
/>)}
/>
</FormItem>
</Paper>
</If>
<Paper elevation={1} className={classes.box}>
<FormItem fullWidth style={{ marginBottom: 10 }}>
<Button
fullWidth
size={'large'}
color={'primary'}
variant={'contained'}
className={classes.submitButton}
onClick={() => this.onSubmit()}
>
<SaveIcon className={classes.rightIcon} />
{Strings.saveText}
</Button>
<Button
fullWidth
size={'large'}
color={'secondary'}
variant={'contained'}
className={classes.cancelButton}
onClick={() => Navigate.goBack()}
>
<CancelIcon className={classes.rightIcon} />
{Strings.cancelText}
</Button>
</FormItem>
</Paper>
</Grid>
</Grid>
</Form>
</React.Fragment>
)}
/>
{/* Get Institutions */}
<Entity
storeId={'Supervision-PlannedVisit-Schedule-List'}
entityRef={ref => { this.getInstitutionsEntity = ref; }}
onEntityReceived={data => this.onInstitutionEntityReceived(data)}
/>
{/* Get Request Id */}
<Entity
storeId={'Supervision-Complaints-GetRequestId'}
entityRef={ref => { this.getRequestIdEntity = ref; }}
onEntityPosted={data => this.getRequestIdEntityPosted(data)}
/>
{/* Get Violation By Request Id --- And Initiate Request in Admin Screens */}
<Entity
storeId={'Supervision-Complaints-Violations-By-ComplaintId'}
entityRef={ref => { this.getViolationsEntity = ref; }}
onEntityReceived={data => this.onViolationsEntityReceived(data)}
onEntityPosted={data => Navigate.goBack()}
/>
</React.Fragment>
);
}
}
violationEditorScreen.propTypes = {
classes: PropTypes.object,
};
export default violationEditorScreen;
componentDidMount() {
if(id) {
// grap the data from back end and upadte the table with checked rows that matches the ids that i got from Back-End
}
}
i expect receive array of Ids then mark each row that it's id is in the array Of Ids to let the user knows What he selected before.
Thx in Advance.
if I understand you correct, you want change the style of the row if it selected, so could you check this url and see the last example in order to modify it to adapt to your situation?
https://material-table.com/#/docs/features/styling
Related
I have a project, and this project contains several interfaces, and among these interfaces there is an interface for uploading an image, and the problem is in the deletion icon. When you click on it, a modal appears, but the element is deleted before the modal appears.
How can i solve the problem?
this file display a list of instructions that contains upload Image
import '../../../styles/input/index.scss';
import '../../../styles/dropzone/index.scss';
import { Button, Col, message, Modal, Row, Spin, Upload, UploadFile } from 'antd';
import { FunctionComponent, useCallback, useRef, useState } from 'react';
import { motion, useAnimation } from 'framer-motion';
import { defaultTranstion } from '../../../constants/framer';
import { Controller } from 'react-hook-form';
import FromElemnetWrapper from '../form-element-wrapper';
import { Delete, UploadCloud } from 'react-feather';
import { getBase64 } from '../../../utils/get-base64';
import _ from 'lodash';
import config from '../../../api/nuclearMedicineApi/config';
import { FormattedMessage } from 'react-intl';
import BasicModal from '../modal';
import { UploadOutlined } from '#ant-design/icons';
import axios from 'axios';
import { IFormError } from '../general-form-containner';
interface DropzoneProps {
name: string;
control: any;
rules?: any;
label: string;
disabled?: boolean;
multiple?: boolean;
accept?: string;
refType?: number;
defaultFileList?: any;
onRemove?: any;
customRequest?: (option: any) => void;
onProgress?: any;
}
const Dropzone: FunctionComponent<DropzoneProps> = ({
name,
control,
rules,
label,
disabled,
multiple,
accept,
refType,
defaultFileList,
onRemove,
customRequest,
onProgress
}) => {
const focusController = useAnimation();
const errorController = useAnimation();
const [previewVisible, setpreviewVisible] = useState(false);
const [previewImage, setpreviewImage] = useState('');
const handleCancel = () => setpreviewVisible(false);
const handlePreview = async (file: any) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setpreviewImage(file?.preview ?? file.url);
setpreviewVisible(true);
};
const [isModalOpen, setIsModalOpen] = useState(false);
const [errors, setErrors] = useState<IFormError[]>([]);
const [visibleModal, setVisibleModal] = useState(false);
const [removePromise, setRemovePromise] = useState();
const [deleteVisible, setdeleteVisible] = useState<boolean>(false);
const onDeleteHandle = () => {
setdeleteVisible(false);
};
const deletehandleCancel = () => {
setdeleteVisible(false);
};
let resolvePromiseRef = useRef<((value: boolean) => void) | undefined>();
// let resolvePromiseRef = useRef<HTMLInputElement | null>(null)
const handleRemove = useCallback(() =>{
const promise = new Promise(resolve => {
resolvePromiseRef.current = resolve
});
setVisibleModal(true);
return promise;
}, [])
const handleOkModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(true)
}
}, [removePromise]);
const handleCancelModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(false);
setVisibleModal(false)
}
}, [removePromise]);
return (
<>
<FromElemnetWrapper
focusController={focusController}
errorController={errorController}
label={label}
required={rules.required?.value}
>
<Controller
control={control}
name={name}
rules={rules}
render={({
field: { onChange, onBlur, value, name, ref },
fieldState: { invalid, error },
}) => {
if (invalid) {
errorController.start({ scale: 80 });
} else {
errorController.start(
{ scale: 0 },
{ ease: defaultTranstion.ease.reverse() },
);
}
return (
<div
onBlur={() => {
onBlur();
focusController.start({ scale: 0 });
}}
onFocus={() => {
focusController.start({ scale: 80 });
}}
className='relative'
>
<div className='upload-container'>
<form
className='dropzone needsclick'
id='demo-upload'
action='/upload'
>
{/* <> */}
<Upload
action={`${config.baseUrl}api/services/app/Attachment/Upload`}
headers={config.headers}
ref={ref}
multiple={multiple}
disabled={disabled}
data={{ RefType: refType }}
listType='picture'
fileList={value}
id={name}
accept={accept}
onPreview={handlePreview}
onRemove={handleRemove}
iconRender={
() => {
return <Spin style={{ marginBottom: '12px', paddingBottom: '12px' }}></Spin>
}
}
progress={{
strokeWidth: 3,
strokeColor: {
"0%": "#f0f",
"100%": "#ff0"
},
style: { top: 12 }
}}
beforeUpload={
(file) => {
console.log({ file });
return true
}
}
// onProgress= {(event: any) => (event.loaded / event.total) * 100}
// onChange={(e) =>
// onChange(e.fileList)
// }
// onChange={(response) => {
// console.log('response: ', response);
// if (response.file.status !== 'uploading') {
// console.log(response.file, response.fileList);
// }
// if (response.file.status === 'done') {
// message.success(`${response.file.name}
// file uploaded successfully`);
// } else if (response.file.status === 'error') {
// message.error(`${response.file.name}
// file upload failed.`);
// }
// else if (response.file.status === 'removed') {
// message.error(`${response.file.name}
// file upload removed.`);
// }
// }}
>
<div className='upload-button'>
<div className='wrapper'>
<motion.div
className='fas fa-angle-double-up'
whileHover={{
y: [
0, -2, 2,
0,
],
transition: {
duration: 1.5,
ease: 'easeInOut',
yoyo: Infinity,
},
}}
>
<UploadCloud
style={{
margin: '.2rem',
display:
'inline-block',
}}
color='white'
size={20}
/>
Upload
</motion.div>
</div>
</div>
</Upload>
{/*
<Modal
title="Are you sure?"
visible={visibleModal}
onOk={handleOkModalRemove}
onCancel={handleCancelModalRemove}
/> */}
<BasicModal
header={
<>
<FormattedMessage id={'confirmdeletion'} />
</>
}
headerType='error'
content={
<>
<Row>
<Col span={8} offset={4}>
<Button
type='primary'
className='savebtn'
onClick={onDeleteHandle}
style={{
cursor:
Object.keys(errors).length !==
0
? 'not-allowed'
: 'pointer',
}}
>
<FormattedMessage id={'affirmation'} />
</Button>
</Col>
<Col span={8} offset={4}>
<Button
type='default'
className='savebtn'
onClick={deletehandleCancel}
style={{
cursor:
Object.keys(errors).length !==
0
? 'not-allowed'
: 'pointer',
}}
>
<FormattedMessage id={'cancel'} />
</Button>
</Col>
</Row>
</>
}
isOpen={visibleModal}
footer={false}
width='35vw'
handleCancel={handleCancelModalRemove}
handleOk={handleOkModalRemove}
/>
{/* {_.isEmpty(value) && (
<div className='dz-message needsclick'>
<FormattedMessage id='dropfileshere' />
</div>
)} */}
<BasicModal
isOpen={previewVisible}
header={<FormattedMessage id="Preview image" />}
footer={false}
handleCancel={handleCancel}
content={<img
alt='example'
style={{ width: '100%' }}
src={previewImage}
/>}
/>
{/* </> */}
</form>
</div>
{invalid && (
<p className='form-element-error'>
{error?.message}
</p>
)}
</div>
);
}}
/>
</FromElemnetWrapper>
</>
);
};
export default Dropzone;
Why it doesn't work?
https://ant.design/components/upload
onRemove - A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is false or a Promise which resolve(false) or reject
You have to return a promise which resolves to false or return false
How to solve it?
You have to return a promise so first of all, you need a ref or a state to store resolve function of that promise so you can call it in modal
const resolvePromiseRef = useRef<((value: boolean) => void) | undefined>();
Then you will need to assign the resolve function to the ref and return the promise
Now onRemove will wait for your promise to resolve
const handleRemove = () =>{
const promise = new Promise(resolve => {
resolvePromiseRef.current = resolve
});
setVisibleModal(true);
return promise;
}
Now your functions handlers
const handleOkModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(true)
}
}, [removePromise]);
const handleCancelModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(false)
}
}, [removePromise]);
You can also use the state, but I recommend ref because it doesn't rerender the component when changed.
Newer to React. I have this column in a ReactTable which has an input textbox. I understand that changing the state is re-rending the form just not sure how to prevent that. Appreciate any guidance!
<ReactTable
key={googlecampaigndata?.length}
data={googlecampaigndata}
loading={googlecampaignloading}
sortable={false}
filterable={false}
columns={[
{
Header: "Account Name",
accessor: "customer_name"
},
{
Header: "Campaign Name",
accessor: "campaign_name"
},
{
Header: "Campaign Type",
accessor: "campaign_type"
},
{
Header: "Status",
accessor: "status"
},
{
Header: "Monthly Budget",
accessor: "monthly_budget"
},
{
Header: "Update Monthly Budget",
accessor: "update_monthly_budget",
Cell: ({ original }) => {
return (
<form
onSubmit={e => {
e.preventDefault();
if(submissionData !== {} && submitGoogleCampaignMonthlyBudgetForm()){
refetch();
}
}
}
>
<input
id={`monthly_budget${original.id}`}
type="text"
maxLength="12"
size="8"
style={{ float: "left" }}
onChange = {updateFieldHandler("monthly_budget", original.id)}
inputprops={{
type: "string",
required: true,
value: original['monthly_budget'] ? original['monthly_budget'] : ""
}}
/><Button
color="primary"
type="submit"
style={{ float: "right" }}
>
Submit
</Button>
</form>
);
},
},
{
Header: "Cycle Start Date",
accessor: "date"
}
]}
defaultPageSize={Math.min(pageSize, googlecampaigndata?.length || 2)}
showPaginationTop
showPaginationBottom={false}
onPageSizeChange={ps => setPageSize(ps)}
className="-striped -highlight"
/>
#EDIT
I tried returning my input field from outside of MyAccounts but I'm still losing focus on the input when typing. More of my updated code below.
import React, { useState, useContext, useEffect } from "react";
import { useQuery, useMutation } from "#apollo/react-hooks";
import { intersection, omit } from "lodash";
// #material-ui/core components
import { makeStyles } from "#material-ui/core/styles";
// #material-ui/icons
import Assignment from "#material-ui/icons/Assignment";
// core components
import GridContainer from "components/Grid/GridContainer";
import GridItem from "components/Grid/GridItem";
import Card from "components/Card/Card";
import CardBody from "components/Card/CardBody";
import CardIcon from "components/Card/CardIcon";
import CardHeader from "components/Card/CardHeader";
import ReactTable from "components/CustomReactTable";
import Button from "components/CustomButtons/Button";
import { cardTitle } from "assets/jss/material-dashboard-pro-react";
import { SUBMIT_GOOGLE_CAMPAIGN_MONTHLY_BUDGET_FORM } from "queries/formSubmission";
import { LIST_BUDGETS } from "queries/budget";
import { GET_GOOGLE_CAMPAIGNS } from "queries/budget";
import { Context } from "redux/store";
const styles = {
cardIconTitle: {
...cardTitle,
marginTop: "15px",
marginBottom: "0px"
}
};
const useStyles = makeStyles(styles);
const GoogleCampaignMonthlyBudgetInput = ({ original, setSubmissionData, submissionData, state, onChange }) => {
return (
<input
id={`monthly_budget${original.id}`}
type="text"
maxLength="12"
size="8"
style={{ float: "left" }}
key={original.id}
onChange={ onChange }
value={submissionData.id === original.id ? submissionData.monthly_budget : original.monthly_budget ? (original.monthly_budget / (1 - (state.customers?.selected?.margin / 100.0))) / 1000000 : ""}
inputprops={{
type: "string",
required: true,
}}
/>
);
}
const MyAccounts = () => {
const [state] = useContext(Context);
const [pageSize, setPageSize] = useState(20);
const customer_id = state.customers?.selected?.id;
const [submissionData, setSubmissionData] = useState({});
let { loading, data } = useQuery(LIST_BUDGETS);
let { loading: googlecampaignloading, data: googlecampaigndata, refetch } = useQuery(GET_GOOGLE_CAMPAIGNS);
googlecampaigndata = googlecampaigndata?.getGoogleCampaigns || [];
data = data?.listBudgets || [];
data = data.map((row, index) => {
let customer_name = "";
let product_line = "";
if (
index % pageSize === 0 ||
row.customer_name !== data[index - 1].customer_name
) {
customer_name = row.customer_name;
}
if (
index % pageSize === 0 ||
row.product_line !== data[index - 1].product_line
) {
product_line = row.product_line;
}
return {
...row,
customer_name,
product_line
};
});
const [submitGoogleCampaignMonthlyBudgetForm, { loading: submitting }] = useMutation(
SUBMIT_GOOGLE_CAMPAIGN_MONTHLY_BUDGET_FORM,
{
variables: {
customerId: customer_id,
data: submissionData
},
onCompleted: () => {
return true;
}
}
);
const classes = useStyles();
return (
<GridContainer>
<GridItem xs={12}>
<Card>
<CardHeader color="trackedKeywords" icon>
<CardIcon>
<Assignment />
</CardIcon>
<h4 className={classes.cardIconTitle}>Google Campaigns</h4>
</CardHeader>
<CardBody>
<ReactTable
key={googlecampaigndata?.length}
data={googlecampaigndata}
loading={googlecampaignloading}
sortable={false}
filterable={false}
columns={[
{
Header: "Monthly Budget",
accessor: "monthly_budget",
Cell: ({ original }) => {
return (
<div>{original.monthly_budget ? (original.monthly_budget / (1 - (state.customers?.selected?.margin / 100.0))) / 1000000 : ""}</div>
);
},
},
{
Header: "Update Monthly Budget",
accessor: "update_monthly_budget",
Cell: ({ original }) => {
return (
<form
onSubmit={e => {
e.preventDefault();
if(submissionData !== {} && submitGoogleCampaignMonthlyBudgetForm()){
refetch();
}
}
}
>
<GoogleCampaignMonthlyBudgetInput original={original} onChange={e => setSubmissionData({...submissionData, "monthly_budget": parseFloat(e.target.value), "id": original.id })} setSubmissionData={setSubmissionData} submissionData={submissionData} state={state} key={original.id} />
<Button
color="primary"
type="submit"
style={{ float: "right" }}
>
Submit
</Button>
</form>
);
},
},
{
Header: "Cycle Start Date",
accessor: "date"
}
]}
defaultPageSize={Math.min(pageSize, googlecampaigndata?.length || 2)}
showPaginationTop
showPaginationBottom={false}
onPageSizeChange={ps => setPageSize(ps)}
className="-striped -highlight"
/>
</CardBody>
</Card>
</GridItem>
</GridContainer>
);
};
export default MyAccounts;
Edit 2: code that I got working with inwerpsel's help!
{
Header: "Monthly Budget",
accessor: "monthly_budget",
Cell: ({ original }) => {
return (
<div>{submissionData.id === original.id ? submissionData.monthly_budget : original.monthly_budget ? (original.monthly_budget / (1 - (state.customers?.selected?.margin / 100.0))) / 1000000 : ""}</div>
);
},
},
{
Header: "Update Monthly Budget",
accessor: "update_monthly_budget",
Cell: ({ original }) => {
const [textInput, setTextInput] = useState(null);
const [submitGoogleCampaignMonthlyBudgetForm, { loading: submitting }] = useMutation(
SUBMIT_GOOGLE_CAMPAIGN_MONTHLY_BUDGET_FORM,
{
variables: {
customerId: customer_id,
data: submissionData
},
onCompleted: () => {
refetch();
return true;
}
}
);
return (
<form
key={"form"+original.id}
onSubmit={e => {
e.preventDefault();
if(Number.isNaN(textInput) || textInput === "" || textInput === null){
setSubmissionData({});
return;
}
submissionData["monthly_budget"] = parseFloat(textInput);
submissionData["id"] = original.id;
if(submissionData !== {} && submitGoogleCampaignMonthlyBudgetForm()){
refetch();
setSubmissionData({});
}
}
}
>
<input
id={`monthly_budget${original.id}`}
type="text"
maxLength="12"
size="8"
style={{ float: "left" }}
key={original.id}
onChange = {event => { setTextInput(event.target.value) }}
defaultValue={submissionData.id === original.id ? submissionData.monthly_budget : original.monthly_budget ? (original.monthly_budget / (1 - (state.customers?.selected?.margin / 100.0))) / 1000000 : ""}
inutprops={{
type: "string",
required: true,
}}
/>
<Button
color="primary"
type="submit"
style={{ float: "right" }}
>
Submit
</Button>
</form>
);
},
},
This GitHub issue seems to indicate that indeed cells will remount as your data changes, even if it's a small change.
In other words, as you type it replaces the whole row with a new instance every character. The new instance has no link to the previous one, even though it should contain the data you typed. As a result it won't have focus.
It seems potentially hard to get around, though I'm not that familiar with react-table.
However, as you already have a form with a submit handler set up, I guess you can just store the input in a local state, and only set it on submit.
Cell: ({ original }) => {
const [textInput, setTextInput] = useState(null);
return (
<form
onSubmit={e => {
e.preventDefault();
// Call the update handler here instead.
const handler = updateFieldHandler("monthly_budget", original.id);
handler(textInput);
if(submissionData !== {} && submitGoogleCampaignMonthlyBudgetForm()){
refetch();
}
}
}
>
<input
// ...
onChange = {event => { setTextInput(event.target.value) }}
// ...
/><Button
color="primary"
type="submit"
style={{ float: "right" }}
>
Submit
</Button>
</form>
);
I have this detail page when the detail button is clicked it goes to the detail page. However, sometimes the page won't load. The fatchData function has 3 items and each one calls the webapi to get different dataset. If I remove any of 2 in fatchData function and only leave one in it and it will load fine. However, the rest of the data will be missing and I need it in the page. How do I solve it? I think it has something to do with the useEffect hook that causes the stackover flow.
Here is the error it always hit: throw Error( "Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops." );
Here is the entire page code:
import React, { useEffect, useState } from 'react';
import 'dotenv/config';
import Button from '#mui/material/Button';
import MenuItem from '#mui/material/MenuItem';
import Select from '#mui/material/Select';
import ClearIcon from '#mui/icons-material/Clear';
import ListItem from '#mui/material/ListItem';
import ListItemIcon from '#mui/material/ListItemIcon';
import ListItemButton from '#mui/material/ListItemButton';
import ListItemText from '#mui/material/ListItemText';
import List from '#mui/material/List';
import { useParams } from 'react-router-dom'
// Fields
import TextField from '#mui/material/TextField';
// Table
import scriptResultTableColumns from './../table/ScriptGroupResultTable';
import TableComponent from './../table/TableComponent';
import { InputLabel } from '#mui/material';
import FormControl from '#mui/material/FormControl';
function ScriptGroupResultsTable(props) {
const columns = scriptResultTableColumns();
return (<TableComponent columns={columns} url={`${process.env.REACT_APP_API_URL}/api/script-
group-result/script-group-results-by-group-id?id=` + props.groupid} buttonVisible={false}
tableType={'script-group-detail'} />);
}
export default function ScriptDetailsPage(props) {
const [validationError] = useState(null);
const [item, setItem] = useState([]);
const [scriptList, setScriptList] = useState({ items:[] });
const [selectedScript, setSelectedScript] = useState('');
const [selectedScripts, setSelectedScripts] = useState([]);
let { id } = useParams();
const [scriptDetailId] = useState(id);
const [url, setUrl] = useState(`${process.env.REACT_APP_API_URL}/api/script-group/script-group?id=` + scriptDetailId);
function fetchData() {
const fetchItem =fetch(url).then(response => response.json()).then(json => {
setItem(json);
},
(error) => {
console.log(error);
});
const fetchScriptList = fetch(`${process.env.REACT_APP_API_URL}/api/script/scripts`).then(response => response.json()).then(json => {
setScriptList(json);
},
(error) => {
console.log(error);
});
const fetchSelectedScripts = fetch(`${process.env.REACT_APP_API_URL}/api/script-group-member/script-group-members-by-group-id?id=` + scriptDetailId).then(groupResponse => groupResponse.json()).then(groupJson => {
groupJson = groupJson.map((member, i) => ({ ...member, index: i }));
setSelectedScripts(groupJson);
},
(groupError) => {
console.log(groupError);
setSelectedScripts([]);
});
Promise.all([fetchItem, fetchScriptList, fetchSelectedScripts]);
}
function onCancel(e) {
fetchData();
}
function onSubmit(e) {
const requestOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
};
var scriptList = [];
for (var i = 0; i < selectedScripts.length; i++) {
scriptList.push(selectedScripts[i].scriptId);
}
const updateMembersOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(scriptList)
}
fetch(`${process.env.REACT_APP_API_URL}/api/script-group?id=` + scriptDetailId, requestOptions).then(
fetch(`${process.env.REACT_APP_API_URL}/api/script-group/replace-group-scripts?id=` + scriptDetailId, updateMembersOptions)
).then(setUrl(url));
}
function getMaxIndex() {
return Math.max.apply(Math, selectedScripts.map(function (o) { return o.index })) + 1;
}
const handleScriptChange = (event) => {
const {
target: { value },
} = event;
var newScriptVal = { ...value, index: getMaxIndex() };
setSelectedScripts([...selectedScripts, newScriptVal])
setSelectedScript('');
};
function removeScript(e) {
if (e.currentTarget.id) {
var index = parseInt(e.currentTarget.id);
setSelectedScripts(selectedScripts.filter((s) => s.index !== index));
}
}
function getBoolStringVal(value) {
if (value)
return "Yes";
else
return "No";
}
useEffect(fetchData, [url, scriptDetailId]);
return (
<div style={{ marginLeft: '10%', maxWidth: '80%' }}>
<TextField
id="groupName"
label="Group Name"
error={validationError}
margin="normal"
fullWidth
value={item.groupName || ''}
onChange={e => setItem({ ...item, groupName: e.target.value })}
/>
<br />
<TextField
aria-readonly
id="groupState"
label="State"
fullWidth
error={validationError}
margin="normal"
value={item.currentStateDescription || ''}
/>
<br />
<TextField
aria-readonly
id="continueOnFailure"
label="Continues on Failure"
error={validationError}
margin="normal"
value={getBoolStringVal(item.continueOnFailure) || ''}
/>
<br />
<FormControl margin="normal">
<InputLabel id="script-select-label" >Add Script</InputLabel>
<Select
labelId="script-select-label"
label="Add Script"
value={selectedScript}
onChange={handleScriptChange}
style={{ width: '350px' }}
>
{
scriptList.items.map((script) => (
<MenuItem
key={script.scriptId}
value={script}
>
{script.scriptName}
</MenuItem>
))}
</Select>
</FormControl>
<br />
<h4>Current Scripts:</h4>
<List dense style={{ width: '300px' }}>
{selectedScripts.map((script) => (
<ListItem key={script.index}>
<ListItemButton>
<ListItemIcon>
<ClearIcon id={script.index} onClick={removeScript} />
</ListItemIcon>
<ListItemText primary={script.scriptName} />
</ListItemButton>
</ListItem>
))}
</List>
<Button variant='outlined' aria-label='Update' style={{ margin: 10, float: 'right' }} onClick={onSubmit}>Update</Button>
<Button variant='outlined' aria-label='Cancel' style={{ margin: 10, float: 'right' }} onClick={onCancel}>Cancel</Button>
<ScriptGroupResultsTable style={{ marginTop: '100px' }} groupid={scriptDetailId} />
</div>
);
}
I have two pages and two components LibraryPageFilters.tsx (url: /courses) and UserVideoCreatePage.tsx (url: /ugc/courses/${course.id}).
In component LibraryPageFilters.tsx
useEffect(() => {
console.log(course.id)
if (course.id) {
console.log(544)
dispatch(push(`/ugc/courses/${course.id}`));
}
}, [course]);
i have a check that if course.id present in the store, then we make a redirect.
In component UserVideoCreatePage.tsx
useEffect(() => {
return () => {
console.log(333344444)
dispatch(courseDelete());
};
}, []);
i am deleting a course from the store when componentUnmount.
why does unmount happen after a redirect? as a result, I am redirected back. Because the course is not removed from the store at the moment of unmount, and the check (if (course.id)) shows that the course is in the store and a redirect occurs back (dispatch(push(/ugc/courses/${course.id})))
UserVideoCreatePage.tsx
import React, { useEffect, useRef, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { Container } from 'Core/components/Container/Container';
import { Svg } from 'Core/components/Svg';
import { Button } from 'Core/Molecules/Button';
import { Select } from 'Core/Molecules/Select';
import {
agreementCourse, categoriesSelector,
courseDelete,
courseEditorCourseSelector,
courseUpdateApi, getCategories,
getCourse,
updateCourseApi,
} from 'Learnings/store/courseEdit';
import { CourseComments } from 'Learnings/screens/CoursePlayPage/CourseBottom/CourseComments/CourseComments';
import { AddItem } from './AddItem';
import s from 'Admin/Pages/Course/Description/index.scss';
import './UserVideoCreatePage.scss';
export const UserVideoCreatePage: React.FC = () => {
const dispatch = useDispatch();
const { id: idCourse } = useParams();
const course = useSelector(courseEditorCourseSelector, shallowEqual);
const categories = useSelector(categoriesSelector, shallowEqual);
const [value, valueSet] = useState({ name: '', description: '', categories: [], lectures: [], materials: [] });
const [tab, tabSet] = useState('program');
const inputFileRef = useRef<HTMLInputElement>(null);
const img = course.gallery_items && course.gallery_items[0];
console.log(categories);
const handleBtnClick = () => {
if (inputFileRef && inputFileRef.current) {
inputFileRef.current.click();
}
};
useEffect(() => {
dispatch(getCourse(idCourse));
dispatch(getCategories());
}, [idCourse]);
useEffect(() => {
valueSet({
name: course.name,
description: course.description,
categories: course.categories && course.categories[0] && course.categories[0].id,
lectures: course.lectures,
materials: course.materials,
});
}, [course]);
useEffect(() => {
return () => {
console.log(333344444)
dispatch(courseDelete());
};
}, []);
return (
<Container className="createCourse">
<Link to="/" className="gallery__back">
<Svg name="arrow_back" width={26} height={20} className="gallery__svg"/>
<span>Назад</span>
</Link>
<div className="createCourse__twoColumn">
<div className="createCourse__twoColumn-left">
<div className="inputBlock">
<label className="inputBlock__label" htmlFor="video">
Название видео-курса
</label>
<input
id="video"
type="text"
placeholder="Введите название вашего видео"
className="inputBlock__input"
value={value.name || ''}
onChange={e =>
valueSet({
...value,
name: e.target.value,
})
}
onBlur={e => {
if (e.target.value && course.name !== e.target.value) {
dispatch(updateCourseApi(idCourse, { name: e.target.value }));
}
}}
/>
</div>
<div className="inputBlock">
<label className="inputBlock__label" htmlFor="opisanie">
Описание видео-курса
</label>
<textarea
id="opisanie"
placeholder="Введите краткое описание вашего видео"
className="inputBlock__input"
value={value.description || ''}
onChange={e =>
valueSet({
...value,
description: e.target.value,
})
}
onBlur={e => {
if (e.target.value && course.description !== e.target.value) {
dispatch(updateCourseApi(idCourse, { description: e.target.value }));
}
}}
/>
</div>
<Select
title="Категории видео-курса"
placeholder="Категории видео-курса"
value={value.categories}
options={categories.map(category => ({ value: category.id, label: category.name }))}
onChange={val => {
valueSet({
...value,
categories: val,
});
dispatch(
updateCourseApi(idCourse, {
category_ids: val,
courses_curators: {
'': {
user_id: val,
},
},
}),
);
}}
search
/>
</div>
<div className="createCourse__twoColumn-right">
<div className="loadVideo">
<div className="loadVideo__field">
<div className="loadVideo__field--block">
{!img && (
<>
<Svg className="loadVideo__field--block-icon" name="icn-load" width={104} height={69}/>
<p className="loadVideo__field--block-text">Загрузите обложку к видео</p>
</>
)}
{img && <img src={img && img.image_url} alt=""/>}
</div>
</div>
<div className="loadVideo__under">
<div className="loadVideo__under--left">
<div className="loadVideo__under--text">
<span className="loadVideo__under--text-grey">*Рекомендуемый формат</span>
<span className="loadVideo__under--text-bold"> 356х100</span>
</div>
<div className="loadVideo__under--text">
<span className="loadVideo__under--text-grey">*Вес не должен превышать</span>
<span className="loadVideo__under--text-bold"> 10 Мб</span>
</div>
</div>
<div className="loadVideo__under--right">
<input
onChange={val => {
if (val.target.files[0]) {
if (img) {
dispatch(
updateCourseApi(idCourse, {
gallery_items: {
'': {
image: val.target.files[0],
id: img.id,
},
},
}),
);
} else {
dispatch(
updateCourseApi(idCourse, {
gallery_items: {
'': {
image: val.target.files[0],
},
},
}),
);
}
}
}}
type="file"
ref={inputFileRef}
className="Library__btn"
/>
<Button
onClick={() => {
handleBtnClick();
}}
>
Библиотека обложек
</Button>
</div>
</div>
</div>
</div>
</div>
<div className={`block-switcher block-switcher--courseCreate`}>
<div
className={`block-switcher__item ${tab === 'program' && 'block-switcher__item_active'}`}
onClick={() => tabSet('program')}
>
Программы
</div>
<div
className={`block-switcher__item ${tab === 'comments' && 'block-switcher__item_active'}`}
onClick={() => tabSet('comments')}
>
Комментарии эксперта
</div>
</div>
{tab === 'program' && (
<>
<AddItem
accept="video/mp4,video/x-m4v,video/*"
fieldName="name"
addType="lecture_type"
title="Видео-курсы"
addBtn="Добавить видео"
type="lectures"
file="video"
lecturesArg={course.lectures}
value={value}
onChangeInput={lecturesNew => {
valueSet({
...value,
lectures: lecturesNew,
});
}}
onVideoUpdate={(params: any) => {
dispatch(updateCourseApi(idCourse, params));
}}
posMove={(lectures: any) => {
dispatch(courseUpdateApi({ id: idCourse, lectures: lectures }, true));
}}
/>
<AddItem
accept=""
fieldName="title"
addType="material_type"
title="Материалы к видео-курсам"
addBtn="Добавить файл"
type="materials"
file="document"
lecturesArg={course.materials}
value={value}
onChangeInput={lecturesNew => {
valueSet({
...value,
materials: lecturesNew,
});
}}
onVideoUpdate={(params: any) => {
dispatch(updateCourseApi(idCourse, params));
}}
posMove={(lectures: any) => {
dispatch(courseUpdateApi({ id: idCourse, materials: lectures }, true));
}}
/>
</>
)}
{tab === 'comments' && <CourseComments title="Обсуждение"/>}
<Button
className={`${s.button} agreement__btn`}
size="big"
onClick={() =>
dispatch(
agreementCourse(idCourse, {
visibility_all_users: true,
}),
)
}
>
Отправить на согласование
</Button>
</Container>
);
};
LibraryPageFilters.tsx
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { push } from 'connected-react-router';
import { getSettingsGlobalSelector } from 'Core/store/settings';
import { Svg } from 'Core/components/Svg';
import { NavBar } from 'Core/Organisms/NavBar';
import { Button } from 'Core/Molecules/Button';
import { CategoriesFilter } from 'Core/Organisms/Filters/components/CategoriesFilter';
import { courseDelete, courseEditorCourseSelector, createNewCourse } from 'Learnings/store/courseEdit';
import { FILTERS, LINKS } from '../../libraryPageConstants';
import { setLibraryPageQuery } from '../../actions/libraryPageActions';
import { getLibraryPageQuerySelector } from '../../libraryPageSelectors';
import s from './index.scss';
import { languageTranslateSelector } from 'Core/store/language';
import { LanguageType } from 'Core/models/LanguageSchema';
import { Status, Tabs } from 'Core/components/Tabs/Tabs';
const statuses: Array<Status> = [
{
key: 'Filter/all',
link: '/courses' || '/courses',
type: 'all' || '',
},
{
key: 'Filter/online',
link: '/courses/online',
type: 'online',
},
{
key: 'Filter/offline',
link: '/courses/offline',
type: 'offline',
},
{
key: 'Filter/complete',
link: '/courses/complete',
type: 'complete',
},
];
export const LibraryPageFilters = () => {
const dispatch = useDispatch();
const [searchTerm, setSearchTerm] = useState('');
const [isBtnDisabled, setIsBtnDisabled] = useState(false);
const course = useSelector(courseEditorCourseSelector, shallowEqual);
const global = useSelector(getSettingsGlobalSelector);
const query = useSelector(getLibraryPageQuerySelector, shallowEqual);
const courseCreateButtonText = useSelector(
languageTranslateSelector('CoursePage/courseCreateButton'),
) as LanguageType;
const { category_id: categoryID } = query;
console.log(course)
useEffect(() => {
console.log(course.id)
if (course.id) {
console.log(544)
dispatch(push(`/ugc/courses/${course.id}`));
}
}, [course]);
useEffect(() => {
return () => {
setIsBtnDisabled(false);
dispatch(courseDelete());
};
}, []);
const onFilter = (values: any) => {
return false;
};
const handleActiveCategory = (id: number) => {
const categoryParam = {
...query,
offset: 0,
category_id: id,
};
if (id === categoryID) {
delete categoryParam.category_id;
}
dispatch(setLibraryPageQuery(categoryParam));
};
const handleSearch = () => {
dispatch(setLibraryPageQuery({ query: searchTerm }));
};
return (
<React.Fragment>
<div className={s.filters}>
{global.coursesPage?.filters.length ? (
<NavBar
className={s.navBar}
links={global.coursesPage.filtersLinks.map(linkType => LINKS[linkType])}
filters={global.coursesPage.filters.map(filterType => FILTERS[filterType])}
onFilter={onFilter}
postfix={
global.coursesPage.courseCreateButton && global.coursesPage.courseCreateButton.enable ? (
<Button
className="coursePageCreateButton"
onClick={() => {
dispatch(createNewCourse());
setIsBtnDisabled(true);
}}
disabled={isBtnDisabled}
>
{courseCreateButtonText['CoursePage/courseCreateButton']}
</Button>
) : null
}
/>
) : (
<div className="track-page__header" data-tut="track-header">
<Tabs statuses={statuses} />
<div className={s.filtersSearch}>
<Svg className={s.filtersSearchIcon} name="search_alternative" width={18} height={18} />
<input
type="text"
placeholder="Поиск"
className={s.filtersSearchInput}
value={searchTerm}
onChange={event => setSearchTerm(event.target.value)}
/>
<button type="button" className={s.filtersButton} onClick={handleSearch}>
Найти
</button>
</div>
</div>
)}
</div>
<CategoriesFilter onChange={handleActiveCategory} selectedID={categoryID} />
</React.Fragment>
);
};
Although react suggests to use Functional Component, try Class Component, I faced similar issues, this was resolved easily in Class Component :
componentDidMount();
componentDidUpdate(prevProps, prevState, snapshot);
These two will solve your problem. Ask me if anything you need.
I am having an issue when routing in Microsoft edge. I have 3 pages and they work like a wizard.
getting-started>start-date>proposer
The application jumps back to getting-started, when I go from start-date to proposer. really weird behaviour and only happing in Edge browser.
Please find the code below,
Start date:
import React, { Component } from "react";
import {
Button,
Form,
Header,
Container,
Grid,
Message
} from "semantic-ui-react";
import { WizardStepper } from "./WizardStepper";
import { startDateActions } from "../redux/actions/startdate-actions";
import { connect } from "react-redux";
import moment from "moment";
import { QuoteRequest } from "../models/quote-request";
import { IStoreState } from "../redux/reducers";
import { AuthResponse } from "../models/auth-response";
import { quoteActions } from "../redux/actions/quote-actions";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
interface IStartDateProps {
history: any;
addQuoteStartDate(quoteStartDate: string): void;
clearErrors(quoteRequest: QuoteRequest): void;
quoteRequest: QuoteRequest;
quoteStartDate: string;
}
export class _StartDate extends Component<IStartDateProps> {
state = {
startDate: new Date(),
showDateEmptyError: false
};
constructor(props: IStartDateProps) {
super(props);
this.state.startDate = this.props.quoteStartDate
? moment(this.props.quoteStartDate).toDate()
: new Date();
}
render() {
return (
<Container fluid>
<WizardStepper
activeStep={2} />
<Container className="basic padded segment abg___main_content_wrapper">
<Header as="h1" className="page-title">
Choose the Start Date
</Header>
<p>
The start date for cover can be from today's date up to 45 days
ahead; we cannot back-date cover.
</p>
<div className="ui hidden divider" />
<Grid stackable>
<Grid.Row>
<Grid.Column mobile={16} tablet={9} computer={8}>
<Form>
<label>What date would you like the cover to start?</label>
<Container className="abg___datepicker">
<DatePicker
className={
this.state.showDateEmptyError ? "error-datepicker" : ""
}
autoComplete="off"
dateFormat="dd/MM/yyyy"
minDate={moment().toDate()}
maxDate={moment()
.add(45, "day")
.toDate()}
onChange={(date: any) => {
this.setState({ startDate: date });
}}
placeholderText="Start date (DD/MM/YYYY)"
selected={this.state.startDate}
/>
{this.state.showDateEmptyError === true ? (
<Message negative>
Please complete the highlighted mandatory field
</Message>
) : null}
<div className="ui hidden divider" />
<Button
size="large"
positive
className="right floated"
onClick={() => {
if (!this.state.startDate) {
this.setState({ showDateEmptyError: true });
} else {
this.setState({ showDateEmptyError: false });
this.props.addQuoteStartDate(
moment(this.state.startDate).format()
);
//Note:This will clear any errors if you had any errors in the next quote proposer screen
this.props.clearErrors(this.props.quoteRequest);
this.props.history.push("/quote/quote-proposer");
}
}}
>
Next
</Button>
</Container>
</Form>
</Grid.Column>
</Grid.Row>
</Grid>
</Container>
</Container>
);
}
}
const mapStateToProps = ({
quoteStartDate,
authResponse,
quoteRequest
}: IStoreState): {
quoteStartDate: string;
authResponse: AuthResponse;
quoteRequest: QuoteRequest;
} => {
return { quoteStartDate, authResponse, quoteRequest };
};
const mapDispatchToProps = (dispatch: any) => {
return {
addQuoteStartDate: (quoteStartDate: string) => {
return dispatch(startDateActions.addStartDate(quoteStartDate));
},
clearErrors: (quoteRequest: QuoteRequest) => {
return dispatch(quoteActions.clearErrors(quoteRequest));
}
};
};
export const StartDate = connect(
mapStateToProps,
mapDispatchToProps
)(_StartDate);
Proposer:
import React from "react";
import {
Button,
Form,
Header,
Container,
Message,
Grid
} from "semantic-ui-react";
import { WizardStepper } from "./WizardStepper";
import { QuoteRequest, Proposer } from "../models/quote-request";
import { connect } from "react-redux";
import { quoteActions } from "../redux/actions/quote-actions";
import { IStoreState } from "../redux/reducers";
import { QuoteType } from "../models/enums";
import { validation } from "../util/validation";
import ErrorMessage from "./validators/ErrorMessage";
import { TitleSelector } from "./shared/TitleSelector";
interface IQuoteProposerProps {
history: any;
quoteStartDate: string;
createQuote(quoteRequest: QuoteRequest): void;
updateQuote(quoteId: number, quoteRequest: QuoteRequest): void;
resetQuoteUpdate(quoteRequest: QuoteRequest): void;
quoteType: QuoteType;
quoteRequest: QuoteRequest;
clearErrors(quoteRequest: QuoteRequest): void;
}
export class _QuoteProposer extends React.Component<IQuoteProposerProps> {
state = {
title: "",
firstName: "",
surname: "",
companyName: "",
contactEmail: "",
contactPhone: "",
showFirstNameEmptyError: false,
showSurNameEmptyError: false,
isTitleSelected: "",
showInvalidEmailError: false,
showInvalidPhoneError: false,
isValidForm: null,
areMandatoryFieldsEmpty: false
};
constructor(props: IQuoteProposerProps) {
super(props);
if (this.props.quoteRequest.proposer) {
this.state.companyName = this.props.quoteRequest.proposer.companyName
? this.props.quoteRequest.proposer.companyName
: "";
this.state.title = this.props.quoteRequest.proposer.salutation
? this.props.quoteRequest.proposer.salutation
: "";
this.state.firstName = this.props.quoteRequest.proposer.firstName
? this.props.quoteRequest.proposer.firstName
: "";
this.state.surname = this.props.quoteRequest.proposer.lastName
? this.props.quoteRequest.proposer.lastName
: "";
this.state.contactEmail = this.props.quoteRequest.proposer.contactEmail
? this.props.quoteRequest.proposer.contactEmail
: "";
this.state.contactPhone = this.props.quoteRequest.proposer.contactPhone
? this.props.quoteRequest.proposer.contactPhone
: "";
}
}
handleCreateQuote = (event: any) => {
event.preventDefault();
this.checkMandatoryFieldsEmpty();
if (this.isValidForm()) {
let quoteRequest = new QuoteRequest();
quoteRequest.startDate = this.props.quoteStartDate;
quoteRequest.proposer = new Proposer();
quoteRequest.proposer.companyName = !this.state.companyName
? null
: this.state.companyName;
quoteRequest.proposer.firstName = this.state.firstName;
quoteRequest.proposer.lastName = this.state.surname;
quoteRequest.proposer.salutation = this.state.title;
quoteRequest.proposer.contactEmail = !this.state.contactEmail
? null
: this.state.contactEmail;
quoteRequest.proposer.contactPhone = !this.state.contactPhone
? null
: this.state.contactPhone;
if (
this.props.quoteRequest.quoteDetails &&
this.props.quoteRequest.quoteDetails.quoteId
) {
this.props.updateQuote(
this.props.quoteRequest.quoteDetails.quoteId,
quoteRequest
);
} else {
this.props.createQuote(quoteRequest);
}
} else {
this.setState({ isValidForm: false });
}
};
getSnapshotBeforeUpdate(prevProps: IQuoteProposerProps, prevState: any) {
return null;
}
componentDidUpdate(
prevProps: IQuoteProposerProps,
prevState: any,
snapshot: any
) {
//When creating
if (
prevProps.quoteRequest.isQuoteCreated !==
this.props.quoteRequest.isQuoteCreated
) {
if (this.props.quoteRequest.isQuoteCreated) {
this.props.clearErrors(this.props.quoteRequest);
if (this.props.quoteType === QuoteType.Simple) {
this.props.history.push("/quote/simple-risk");
} else {
this.props.history.push("/quote/complex-risk");
}
}
}
//When updating
if (
prevProps.quoteRequest.isQuoteUpdated !==
this.props.quoteRequest.isQuoteUpdated
) {
if (this.props.quoteRequest.isQuoteUpdated) {
this.props.clearErrors(this.props.quoteRequest);
this.props.clearErrors(this.props.quoteRequest);
if (this.props.quoteType === QuoteType.Simple) {
this.props.history.push("/quote/simple-risk");
} else {
this.props.history.push("/quote/complex-risk");
}
}
}
}
isValidForm = (): boolean => {
let isValid = true;
if (validation.isEmpty(this.state.firstName)) {
this.setState({ showFirstNameEmptyError: true });
isValid = false;
} else {
this.setState({ showFirstNameEmptyError: false });
}
if (validation.isEmpty(this.state.surname)) {
this.setState({ showSurNameEmptyError: true });
isValid = false;
} else {
this.setState({ showSurNameEmptyError: false });
}
if (validation.isEmpty(this.state.title)) {
this.setState({ isTitleSelected: "false" });
isValid = false;
} else {
this.setState({ isTitleSelected: "true" });
}
if (
!validation.isEmpty(this.state.contactEmail) &&
!validation.isEmail(this.state.contactEmail)
) {
isValid = false;
this.setState({ showInvalidEmailError: true });
} else {
this.setState({ showInvalidEmailError: false });
}
if (
!validation.isEmpty(this.state.contactPhone) &&
!validation.isPhone(this.state.contactPhone)
) {
isValid = false;
this.setState({ showInvalidPhoneError: true });
} else {
this.setState({ showInvalidPhoneError: false });
}
return isValid;
};
checkMandatoryFieldsEmpty = () => {
if (
validation.isEmpty(this.state.firstName) ||
validation.isEmpty(this.state.surname) ||
this.state.isTitleSelected === "false"
) {
this.setState({ areMandatoryFieldsEmpty: true });
} else {
this.setState({ areMandatoryFieldsEmpty: false });
}
};
handleMandatoryFieldsCheck = (event: any) => {
this.checkMandatoryFieldsEmpty();
};
render() {
return (
<Container fluid>
<WizardStepper activeStep={3} />
<Container className="padded abg___main_content_wrapper">
<Header as="h1" className="page-title">
About the Proposer
</Header>
<p>Please tell us about the proposer.</p>
<div className="ui hidden divider" />
<Grid stackable doubling>
<Grid.Row>
<Grid.Column mobile={16} tablet={9} computer={8}>
<Form
onSubmit={this.handleCreateQuote}
autoComplete="off"
noValidate
>
<Container>
<Form.Field>
<label>Company name</label>
<Form.Input
type="text"
maxLength={30}
name="company-name"
value={this.state.companyName}
placeholder="Company Name"
onChange={(event: any) => {
this.setState({
companyName: event.target.value
});
}}
/>
</Form.Field>
<Form.Field>
<TitleSelector
onClick={async (data: any) => {
await this.setState({
title: data
});
this.setState({
isTitleSelected: !validation.isEmpty(
this.state.title
)
? "true"
: "false"
});
}}
showTitleNotSelectedError={
this.state.isTitleSelected === "false"
}
titles={["Mr", "Mrs", "Miss", "Ms"]}
selectedTitle={this.state.title}
cssClasses="abg___button-options"
/>
</Form.Field>
<Form.Field>
<label>First name</label>
<Form.Input
type="text"
name="first-name"
value={this.state.firstName}
maxLength={30}
error={this.state.showFirstNameEmptyError}
placeholder="First Name"
onChange={async (event: any) => {
await this.setState({
firstName: event.target.value
});
if (!validation.isEmpty(this.state.firstName)) {
this.setState({ showFirstNameEmptyError: false });
}
}}
/>
</Form.Field>
<Form.Field>
<label>Surname</label>
<Form.Input
type="text"
name="surname"
maxLength={30}
value={this.state.surname}
error={this.state.showSurNameEmptyError}
placeholder="Surname"
onChange={async (event: any) => {
await this.setState({ surname: event.target.value });
if (!validation.isEmpty(this.state.surname)) {
this.setState({ showSurNameEmptyError: false });
}
}}
/>
</Form.Field>
<Form.Field>
<label>Contact email</label>
<Form.Input
type="email"
name="email"
maxLength={30}
value={this.state.contactEmail}
error={this.state.showInvalidEmailError}
placeholder="Contact email"
onChange={(event: any) => {
this.setState({ contactEmail: event.target.value });
}}
onBlur={(event: any) => {
this.setState({
showInvalidEmailError: !validation.isEmail(
this.state.contactEmail
)
});
}}
/>
<ErrorMessage
show={this.state.showInvalidEmailError}
message="Invalid email"
/>
</Form.Field>
<Form.Field>
<label>Contact phone</label>
<Form.Input
type="text"
name="tel"
maxLength={30}
value={this.state.contactPhone}
error={this.state.showInvalidPhoneError}
placeholder="Contact phone"
onChange={(event: any) => {
this.setState({ contactPhone: event.target.value });
}}
onBlur={(event: any) => {
this.setState({
showInvalidPhoneError: !validation.isPhone(
this.state.contactPhone
)
});
}}
/>
<ErrorMessage
show={this.state.showInvalidPhoneError}
message="Invalid phone"
/>
</Form.Field>
{this.props.quoteRequest.quoteCreateError ||
this.props.quoteRequest.quoteUpdateError ? (
<Message negative>Server Error, please try again</Message>
) : null}
{this.state.areMandatoryFieldsEmpty ? (
<Message negative>
Please complete the highlighted mandatory fields
</Message>
) : null}
</Container>
<div className="ui hidden divider" />
<Container>
<Button
size="large"
type="submit"
floated="right"
positive
loading={
this.props.quoteRequest.isCreatingQuote ||
this.props.quoteRequest.isUpdatingQuote
}
>
Next
</Button>
</Container>
</Form>
</Grid.Column>
</Grid.Row>
</Grid>
</Container>
</Container>
);
}
}
function mapStateToProps(state: IStoreState) {
return {
quoteStartDate: state.quoteStartDate,
quoteType: state.quoteType,
quoteRequest: state.quoteRequest
};
}
const mapDispatchToProps = (dispatch: any) => {
return {
createQuote: (quoteRequest: QuoteRequest) => {
return dispatch(quoteActions.createQuote(quoteRequest));
},
updateQuote: (quoteId: number, quoteRequest: QuoteRequest) => {
return dispatch(quoteActions.updateQuote(quoteId, quoteRequest));
},
resetQuoteUpdate: (quoteRequest: QuoteRequest) => {
return dispatch(quoteActions.resetQuoteUpdate(quoteRequest));
},
clearErrors: (quoteRequest: QuoteRequest) => {
return dispatch(quoteActions.clearErrors(quoteRequest));
}
};
};
export const QuoteProposer = connect(
mapStateToProps,
mapDispatchToProps
)(_QuoteProposer);
Layout page routes:
render() {
return (
<div id="page-container">
<div id="content-wrap" className="clearfix">
<PageHeaderBar />
<Switch>
<Route
exact
path="/login"
component={Login}
authenticatedRedirect="/quote/getting-started"
/>
<PrivateRoute exact path="/" component={GettingStarted} />
<PrivateRoute
exact
path="/quote/getting-started"
component={GettingStarted}
/>
<PrivateWizardRoute
exact
path="/quote/start-date"
component={StartDate}
wizard={this.props.wizard}
/>
<PrivateWizardRoute
exact
path="/quote/quote-proposer"
component={QuoteProposer}
wizard={this.props.wizard}
/>
<PrivateWizardRoute
exact
path="/quote/simple-risk"
component={SimpleRisk}
wizard={this.props.wizard}
/>
<PrivateWizardRoute
exact
path="/quote/complex-risk"
component={ComplexRisk}
wizard={this.props.wizard}
/>
<PrivateRoute exact path="/quote/summary" component={Summary} />
</Switch>
<PageFooter />
</div>
</div>
);
}
}
PrivateWizardRoute:
import React from "react";
import { Redirect, Route, RouteProps } from "react-router-dom";
import { Wizard } from "../models/wizard";
import { WizardStage } from "../models/enums";
interface IPrivateWizardRouteProps {
wizard: Wizard;
}
export class PrivateWizardRoute extends React.Component<
IPrivateWizardRouteProps & RouteProps
> {
renderRoute = () => {
let isAuthenticated = localStorage.getItem("authResponse") ? true : false;
if (isAuthenticated) {
if (this.props.wizard.wizardStage === WizardStage.InProgress) {
const { path, exact, component } = this.props;
return <Route path={path} exact={exact} component={component} />;
} else {
return (
<Redirect
to={{
pathname: "/quote/getting-started"
}}
/>
);
}
} else {
return <Redirect to={{ pathname: "/login" }} />;
}
};
render() {
return this.renderRoute();
}
}
export default PrivateWizardRoute;
I was missing event.preventDefault StartDate button click.