I am trying to build a user system but I am getting confused with something since it does not work in real time.
I have created an example Sandbox to show my "issue" and my code. I didn't add any kind of validation stuff, it is just for example purposes.
Some of the issues (I see) are:
Button actions trigger on second click
Data won't refresh after created / deleted.
This is the <UsersPage /> component:
import React, { Fragment, useState, useEffect } from "react";
import { useMutation, useLazyQuery } from "#apollo/react-hooks";
import { ADD_USER, LIST_USERS, DELETE_USER } from "../../../config/constants";
import { useSnackbar } from "notistack";
import {
Grid,
Paper,
TextField,
Button,
Typography,
MenuItem,
FormHelperText
} from "#material-ui/core";
import AddUserIcon from "#material-ui/icons/PersonAdd";
import { withStyles } from "#material-ui/core/styles";
import PropTypes from "prop-types";
import Table from "../../Table";
const styles = theme => ({
grid: {
margin: theme.spacing(3)
},
icon: {
marginRight: theme.spacing(2)
},
form: {
width: "100%",
marginTop: theme.spacing(3),
overflowX: "auto",
padding: theme.spacing(2)
},
submit: {
margin: theme.spacing(2)
},
container: {
display: "flex",
flexWrap: "wrap"
},
textField: {
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit
},
root: {
width: "100%",
marginTop: theme.spacing(3),
overflowX: "auto",
padding: theme.spacing(2)
},
title: {
margin: theme.spacing(2)
},
table: {
minWidth: 700
},
noRecords: {
textAlign: "center"
},
button: {
margin: theme.spacing.unit
}
});
const Users = props => {
const [idState, setIdState] = useState(null);
const [emailState, setEmailState] = useState("");
const [passwordState, setPasswordState] = useState("");
const [usersState, setUsersState] = useState([]);
const [errorsState, setErrorsState] = useState({});
const [loadingState, setLoadingState] = useState(false);
const [addUser, addUserResponse] = useMutation(ADD_USER);
const [loadUsers, usersResponse] = useLazyQuery(LIST_USERS);
const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER);
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
loadUsers();
if (usersResponse.called && usersResponse.loading) {
setLoadingState(true);
} else if (usersResponse.called && !usersResponse.loading) {
setLoadingState(false);
}
if (usersResponse.data) {
setUsersState(usersResponse.data.getUsers);
}
}, [usersResponse.called, usersResponse.loading, usersResponse.data]);
function handleSubmit(e) {
e.preventDefault();
if (idState) {
} else {
addUser({
variables: {
email: emailState,
password: passwordState
}
});
}
if (addUserResponse.called && addUserResponse.loading) {
enqueueSnackbar("Creating user");
}
if (addUserResponse.error) {
addUserResponse.error.graphQLErrors.map(exception => {
const error = exception.extensions.exception;
const messages = Object.values(error);
enqueueSnackbar(messages[0], { variant: "error" });
});
}
if (addUserResponse.data && addUserResponse.data.addUser) {
enqueueSnackbar("user created", { variant: "success" });
loadUsers();
}
}
function handleEdit(user) {
setIdState(user.id);
setEmailState(user.email);
}
async function handleDelete(data) {
if (typeof data === "object") {
data.map(id => {
deleteUser({ variables: { id } });
if (deleteUserResponse.data && deleteUserResponse.data.deleteUser) {
enqueueSnackbar("User deleted", { variant: "success" });
}
});
} else {
deleteUser({ variables: { id: data } });
if (deleteUserResponse.data && deleteUserResponse.data.deleteUser) {
enqueueSnackbar("User deleted", { variant: "success" });
}
}
}
function resetForm() {
setIdState(null);
setEmailState("");
}
const { classes } = props;
return (
<Fragment>
<Grid container spacing={8}>
<Grid item xs={3} className={classes.grid}>
<Paper className={classes.form}>
<Typography variant="h6" className={classes.title}>
{idState ? `Edit user: ${emailState}` : "Create user"}
</Typography>
<form className={classes.container} onSubmit={handleSubmit}>
<input type="hidden" name="id" value={idState} />
<TextField
className={classes.textField}
label="E-mail address"
type="email"
variant="outlined"
margin="normal"
autoComplete="email"
id="email"
name="email"
required={!idState}
fullWidth
onChange={e => setEmailState(e.target.value)}
value={emailState}
aria-describedby="email-error"
/>
<FormHelperText id="email-error">
{errorsState.email}
</FormHelperText>
<TextField
className={classes.textField}
label="Password"
variant="outlined"
margin="normal"
autoComplete="password"
id="password"
name="password"
required={!idState}
type="password"
fullWidth
onChange={e => setPasswordState(e.target.value)}
value={passwordState}
aria-describedby="password-error"
/>
<FormHelperText id="password-error">
{errorsState.password}
</FormHelperText>
<Button
variant="contained"
color="primary"
className={classes.submit}
size="large"
type="submit"
>
<AddUserIcon className={classes.icon} /> Save
</Button>
<Button
variant="contained"
color="secondary"
className={classes.submit}
type="button"
onClick={resetForm}
>
<AddUserIcon className={classes.icon} /> Add new
</Button>
</form>
</Paper>
</Grid>
<Grid item xs={8} className={classes.grid}>
<Paper className={classes.root}>
<Table
data={usersState}
className={classes.table}
columns={{
id: "ID",
email: "E-mail address"
}}
classes={classes}
title="Users"
handleEdit={handleEdit}
handleDelete={handleDelete}
filter={true}
loading={loadingState}
/>
</Paper>
</Grid>
</Grid>
</Fragment>
);
};
Users.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(styles)(Users);
In case you need more code, or editing:
Frontend sandbox: App / Code
Backend sandbox: App / Code
Any comments, suggestions or whatever will be appreciated.
Button actions trigger on second click
It's because you call
if (addUserResponse.called && addUserResponse.loading) {
enqueueSnackbar("Creating user");
}
right after you call addUser. The state didn't change when you check if (addUserResponse.called && addUserResponse.loading), the state is the same state before calling addUser.
When you click on the second time, you have the state after the first click and this if
if (addUserResponse.data && addUserResponse.data.addUser) {
enqueueSnackbar("user created", { variant: "success" });
loadUsers();
}
is true.
Solution:
Create a useEffect to handle addUser state and remove the if clauses from handleSubmit
useEffect(() => {
if (!addUserResponse.called) {
return;
}
if (addUserResponse.loading) {
enqueueSnackbar("Creating user");
return;
}
if (addUserResponse.error) {
addUserResponse.error.graphQLErrors.map(exception => {
const error = exception.extensions.exception;
const messages = Object.values(error);
enqueueSnackbar(messages[0], { variant: "error" });
});
return;
}
enqueueSnackbar("user created", { variant: "success" });
}, [addUserResponse.called, addUserResponse.loading]);
function handleSubmit(e) {
e.preventDefault();
if (idState) {
} else {
addUser({
variables: {
email: emailState,
password: passwordState
}
});
}
}
Data won't refresh after created / deleted.
You should update your cache after mutation because Apollo doesn't know if you're adding or deleting when you call a mutation.
Solution:
const [addUser, addUserResponse] = useMutation(ADD_USER, {
update: (cache, { data: { addUser } }) => {
// get current data cache
const cachedUsers = cache.readQuery({ query: LIST_USERS });
// create new users
const newUsers = [addUser, ...cachedUsers.getUsers];
// save newUsers on cache
cache.writeQuery({
query: LIST_USERS,
data: {
getUsers: newUsers
}
});
}
});
The same is true for delete user, expect newUsers will have current users filtered:
const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER, {
update: (cache, { data: { deleteUser } }) => {
const cachedUsers = cache.readQuery({ query: LIST_USERS });
// NOTE: this didn't work because deleteUser return true instead user.
// I'd suggest change your backend and deleteUser return user id to
// be able to perform this filter.
const newUsers = cachedUsers.getUsers.filter(
({ id }) => id !== deleteUser.id
);
cache.writeQuery({
query: LIST_USERS,
data: {
getUsers: newUsers
}
});
}
});
Note 1:
You don't need to call loadUsers more than one time. Because you update the cache when you perform a mutation, your data will always be the most recent. Because of that I'd call loadUsers this way:
useEffect(() => {
loadUsers();
}, []);
useEffect(() => {
if (usersResponse.called && usersResponse.loading) {
setLoadingState(true);
} else if (usersResponse.called && !usersResponse.loading) {
setLoadingState(false);
}
}, [usersResponse.called, usersResponse.loading]);
Note 2
You don't need to create a state for users, you already have one from usersResponse.data.getUsers, but it's your preference. In my case, I removed const [usersState, setUsersState] = useState([]); and added
const users =
usersResponse.data && usersResponse.data.getUsers
? usersResponse.data.getUsers
: [];
to pass to the table.
Edit October 10, 2019
The main change I did was creating a mutation called batchDeleteUsers that delete multiple users in a single call.
Updating server
I've made some changes to the server to get the app working. First, deleteUser returns User and I've created a mutation called batchDeleteUsers.
My current mutation schema:
type Mutation {
addUser(email: String!, password: String!): User
deleteUser(id: String!): User
batchDeleteUsers(ids: [String!]!): [User]
}
My current resolvers:
deleteUser: (root, { id }, context) => {
const user = USERSDB.find(user => user.id === id);
USERSDB = USERSDB.filter(user => user.id !== id);
return user;
},
batchDeleteUsers: (root, { ids }, context) => {
const users = USERSDB.filter(user => ids.includes(user.id));
USERSDB = USERSDB.filter(user => !ids.includes(user.id));
return users;
}
Updating App 1
Instead using useLazyQuery and calling it inside a useEffect, I'm using useQuery. This way we don't need to perform query inside useEffect, it's triggered when component initializes.
const usersResponse = useQuery(LIST_USERS);
Updating App 2
Below is how I create deleteUser and batchDeleteUsers mutations.
const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER, {
update: (cache, { data: { deleteUser } }) => {
const cachedUsers = cache.readQuery({ query: LIST_USERS });
const newUsers = cachedUsers.getUsers.filter(
({ id }) => id !== deleteUser.id
);
cache.writeQuery({
query: LIST_USERS,
data: {
getUsers: newUsers
}
});
}
});
const [batchDeleteUsers, batchDeleteUsersResponse] = useMutation(
BATCH_DELETE_USERS,
{
update: (cache, { data: { batchDeleteUsers } }) => {
const cachedUsers = cache.readQuery({ query: LIST_USERS });
const newUsers = cachedUsers.getUsers.filter(({ id }) => {
return !batchDeleteUsers.map(({ id }) => id).includes(id);
});
cache.writeQuery({
query: LIST_USERS,
data: {
getUsers: newUsers
}
});
}
}
);
Updating App 3
This is how I handle the delete users' mutations lifecycle.
useEffect(() => {
if (!deleteUserResponse.called) {
return;
}
if (deleteUserResponse.loading) {
enqueueSnackbar("Deleting user");
return;
}
if (deleteUserResponse.error) {
deleteUserResponse.error.graphQLErrors.map(exception => {
const error = exception.extensions.exception;
const messages = Object.values(error);
enqueueSnackbar(messages[0], { variant: "error" });
});
return;
}
enqueueSnackbar("user deleted", { variant: "success" });
}, [deleteUserResponse.called, deleteUserResponse.loading]);
useEffect(() => {
if (!batchDeleteUsersResponse.called) {
return;
}
if (batchDeleteUsersResponse.loading) {
enqueueSnackbar("Deleting users");
return;
}
if (batchDeleteUsersResponse.error) {
batchDeleteUsersResponse.error.graphQLErrors.map(exception => {
const error = exception.extensions.exception;
const messages = Object.values(error);
enqueueSnackbar(messages[0], { variant: "error" });
});
return;
}
enqueueSnackbar("users deleted", { variant: "success" });
}, [batchDeleteUsersResponse.called, batchDeleteUsersResponse.loading]);
Updating App 4
Finally, this is how I handle delete users.
function handleDelete(data) {
if (typeof data === "object") {
batchDeleteUsers({ variables: { ids: data } });
} else {
deleteUser({ variables: { id: data } });
}
}
My server sandbox: code
My frontend sandbox: code
Related
I'm new React developer(mainly with hooks but did not find good example with hooks), here i have antd table with search functionality, my question is when user writes something in search then user gets different result, how to cancel that search by clicking 'Reset' button ?
my code:
https://codesandbox.io/s/antd-table-filter-search-forked-mqhcn?file=/src/EventsSection/EventsSection.js
You can add an id to your input into TitleSearch.js:
<Search
id='IDYOUWANT'
placeholder="Enter Title"
onSearch={onSearch}
onChange={onChange}
style={{ width: 200 }}
/>
And add event into EventsSection.js
ResetInput = () => {
const input = document.getElementById('IDYOUWANT');
input.value = '';
this.handleSearch('');
}
....
<button
onClick={this.ResetInput}
>Reset</button>
Change IDYOUWANT with your id
run this code
Created a new function for reset value and trigger it from reset button.
function:
resetValue = () =>{
this.setState({
eventsData: eventsData
});
}
And trigger from button
<button onClick={this.resetValue}>Reset</button>
all code::
import React, { Component } from "react";
import styles from "./style.module.css";
import { EventsTable } from "../EventsTable";
import { StatusFilter } from "../StatusFilter";
import { TitleSearch } from "../TitleSearch";
const eventsData = [
{
key: 1,
title: "Bulletproof EP1",
fileType: "Atmos",
process: "match media",
performedBy: "Denise Etridge",
operationNote: "-",
updatedAt: "26/09/2018 17:21",
status: "complete"
},
{
key: 2,
title: "Dexter EP2",
fileType: "Video",
process: "Compliance",
performedBy: "Dane Gill",
operationNote: "passed",
updatedAt: "21/09/2018 12:21",
status: "inProgress"
}
];
class EventsSection extends Component {
constructor(props) {
super(props);
this.state = {
eventsData
};
}
handleFilter = (key) => {
const selected = parseInt(key);
if (selected === 3) {
return this.setState({
eventsData
});
}
const statusMap = {
1: "complete",
2: "inProgress"
};
const selectedStatus = statusMap[selected];
const filteredEvents = eventsData.filter(
({ status }) => status === selectedStatus
);
this.setState({
eventsData: filteredEvents
});
};
handleSearch = (searchText) => {
const filteredEvents = eventsData.filter(({ title }) => {
title = title.toLowerCase();
return title.includes(searchText);
});
this.setState({
eventsData: filteredEvents
});
};
handleChange = (e) => {
const searchText = e.target.value;
const filteredEvents = eventsData.filter(({ title }) => {
title = title.toLowerCase();
return title.includes(searchText);
});
this.setState({
eventsData: filteredEvents
});
};
resetValue = () =>{
this.setState({
eventsData: eventsData
});
}
render() {
return (
<section className={styles.container}>
<header className={styles.header}>
<h1 className={styles.title}>Events</h1>
<button onClick={this.resetValue}>Reset</button>
<TitleSearch
onSearch={this.handleSearch}
onChange={this.handleChange}
className={styles.action}
/>
</header>
<EventsTable eventsData={this.state.eventsData} />
</section>
);
}
}
export { EventsSection };
Here is what i did in order to solve it:
i added onClick on the button
<button onClick={this.resetSearch}>Reset</button>
Then in the function i put handleSearch to '', by doing this it reset the table:
resetSearch = () =>{
this.handleSearch('')
}
I am having a callback function which updates my array in the SetState and it works fine as I can see in the console log in the handleDaysValueChange all is good. But when I try to access it in another function i.e handleValuesChange the values are not up to date. I am missing a async or something.
import React from 'react';
import PropTypes from 'prop-types';
import AppService from 'services/app-service';
import Enum from 'enum';
import { ApiData } from 'data';
import { isEmpty, boolValue } from 'core/type-check';
import { notifySuccess } from 'core/errors';
import { withDrawerForm } from 'components/_hoc';
import { ExtendedAvatar } from 'components/extended-antd';
import { Tabs } from 'antd';
import { FormTemplateAudit } from '../templates';
import ShiftRosterFormDetails from './shiftroster-form-details';
import Item from 'antd/lib/list/Item';
const { TabPane } = Tabs;
class ShiftRosterForm extends React.Component {
constructor(props) {
super(props);
this.formInputRef = React.createRef();
this.state = {
daysValue:[]
};
this.handleDaysValueChange = this.handleDaysValueChange.bind(this);
}
handleDaysValueChange(daysValues) {
this.setState({ daysValue: daysValues }, () => {
console.log("Data" + this.props.customData);
console.log("Hello World " + JSON.stringify(this.state.daysValue));
});
}
componentDidMount() {
// Update the parent form state with a reference to the
// formInputRef so we can call this later for validation
// before saving the data to the server.
const { onFormStateChange } = this.props;
onFormStateChange({ formInputRef: this.formInputRef.current });
}
//Shift Roster Detail
handleValuesChange = (props, changedValues, allValues) => {
// Update the parent form state with the changed item
// details and mark the form as dirty
const { itemData, onFormStateChange } = this.props;
console.log("Hey" + this.state.daysValue);
onFormStateChange({
isFormDirty: true,
itemData: {
...itemData,
...changedValues,
...this.state.daysValue
}
});
}
render() {
const { itemData, customData } = this.props;
const isSR = (!isEmpty(itemData) && itemData.id > 0);
return (
<Tabs className="bhp-tabs" defaultActiveKey="1" animated={false}>
<TabPane key="1" tab="Details">
<ShiftRosterFormDetails
ref={this.formInputRef}
dataSource={itemData}
onValuesChange={this.handleValuesChange}
handleDaysValueChange={this.handleDaysValueChange}
/>
</TabPane>
<TabPane key="2" tab="Audit" disabled={!isSR}>
<FormTemplateAudit
itemData={itemData}
/>
</TabPane>
</Tabs>
);
}
}
ShiftRosterForm.propTypes = {
itemId: PropTypes.number, // Passed in by the HOC. The loaded Shift Roster id
itemData: PropTypes.object, // Passed in by the HOC. The loaded Shift Roster data
customData: PropTypes.object, // Temporary store to hold the changed Shift Roster
isFormDirty: PropTypes.bool, // Passed in by the HOC. Flags if the parent form is dirty
isLoading: PropTypes.bool, // Passed in by the HOC. Flags if the parent form is loading
daysValue: PropTypes.object,
onFormStateChange: PropTypes.func // Passed in by the HOC. Callback to update the parent form state.
};
ShiftRosterForm.defaultProps = {
itemId: -1,
itemData: {},
customData: {},
isFormDirty: false,
isLoading: false,
daysValue: {},
onFormStateChange() { }
};
const ShiftRosterFormTitle = ({ data }) => {
const name = (!isEmpty(data) && data.id > 0) ? `${data.name}` : 'New Shift Roster';//`${data.name}`
return isEmpty(data)
? <ExtendedAvatar type="icon" size="large" />
: <span><ExtendedAvatar name={name} type="letter" size="large" />{name}</span>
}
const saveShiftRoster = (shiftrosterId, shiftroster, rosterdays) => {
return ApiData.saveShiftRoster(shiftrosterId, shiftroster, rosterdays)
.then(response => {
notifySuccess('Save Successful', 'Site Work Package saved successfully');
return response;
})
.catch(error => {
throw error;
});
}
const saveForm = (formState, setFormState) => {
const { isFormDirty, itemData, customData, formInputRef } = formState;
const typeName = "Dynamic";
const actualType = itemData.type;
let rosterdays = [];
if (actualType !== typeName) {
rosterdays = GetDaysForRoster(itemData);
console.log("My Values" + JSON.stringify(rosterdays));
}
const shiftRosterId = itemData.id;
const isExistingShiftRoster = shiftRosterId > 0;
return new Promise((resolve, reject) => {
if (isExistingShiftRoster && !isFormDirty) {
// No Changes
notifySuccess('Save Successful', 'Site Work Package saved successfully');
resolve(itemData);
}
else {
// Validate and Save
formInputRef.validateFields((error, values) => {
if (!error) {
// Form validated successfully.
// Save form changes
const shiftrosterRecord = saveShiftRoster(shiftRosterId, values, rosterdays);
resolve(shiftrosterRecord);
}
else {
// Form validation error.
// Return data as is.
resolve(itemData);
}
});
}
});
}
const GetDaysForRoster = (itemsData) => {
const result = [];
const keys = Object.keys(itemsData);
for (const k in keys) {
if (Number(k) == k) {
result[k] = itemsData[k]
}
}
return result.filter(function (el) { return el != null });
}
const WrappedShiftRosterForm = withDrawerForm({
containerClassName: 'bhp-equipment-type-form',
title: (record) => <ShiftRosterFormTitle data={record} />,
onLoad: (itemId, setFormState) => ApiData.getShiftRoster(itemId),
onSave: (formState, setFormState) => { return saveForm(formState, setFormState); },
canView: () => AppService.hasAccess({ [Enum.SecurityModule.EquipmentTypeDetails]: [Enum.SecurityPermission.Read] }),
canCreate: () => AppService.hasAccess({ [Enum.SecurityModule.EquipmentTypeDetails]: [Enum.SecurityPermission.Create] }),
canUpdate: () => AppService.hasAccess({ [Enum.SecurityModule.EquipmentTypeDetails]: [Enum.SecurityPermission.Update] })
})(ShiftRosterForm);
WrappedShiftRosterForm.propTypes = {
containerClassName: PropTypes.string,
itemId: PropTypes.number,
visible: PropTypes.bool,
onSave: PropTypes.func,
onClose: PropTypes.func
};
WrappedShiftRosterForm.defaultProps = {
containerClassName: null,
itemId: -1,
visible: false,
onSave() { },
onClose() { }
};
export default WrappedShiftRosterForm;
//ShiftRosterFormDetails
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { ApiData } from 'data';
import { Form, Input, Select, Button, space, InputNumber } from 'antd';
import ShiftDays from './shiftdays'
const ShiftRosterFormDetails = ({ form, dataSource, onValueChange, handleDaysValueChange }) => {
const { getFieldDecorator } = form;
const formLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
},
};
console.log("My datasource" + dataSource.shiftRosterDays);
//const daysRoster = dataSource.shiftRosterDays || [{ daysIn: 1, daysOut: 1, category: "Day Shift" }];
const [inputList, setInputList] = useState([{ daysIn: 1, daysOut: 1, category: "Day Shift" }]);
const [selectedType, setSelectedType] = useState(dataSource.type || 'Fixed Single');
const [isTotalDaysRequired, SetTotalDaysRequired] = useState(dataSource.type === 'Dynamic' ? true : false);
const [isTotalDaysRequiredMessage, setIsTotalDaysRequiredMessage] = useState(dataSource.type === 'Dynamic' ? 'Please enter the Total Days' : '');
const handleTypeChanged = (value, e) => {
setSelectedType(value);
if (value === "Dynamic") {
SetTotalDaysRequired(true);
setIsTotalDaysRequiredMessage('Please enter the Total Days');
}
if (value === "Fixed Single") {
if (inputList.length > 1) {
const list = [...inputList];
console.log("Total" + inputList.length);
list.splice(1, inputList.length);
setInputList(list);
console.log("My List" + JSON.stringify(list));
console.log("Input List" + JSON.stringify(inputList));
handleDaysValueChange(list);
}
}
else {
SetTotalDaysRequired(false);
setIsTotalDaysRequiredMessage('');
}
};
return (
<div className='bhp-equipment-type-form-details bhp-content-box-shadow'>
<Form {...formLayout}>
<Form.Item label='Name' hasFeedback>
{getFieldDecorator('name', {
initialValue: dataSource.name,
rules: [{
required: true,
message: 'Please enter the Name'
}],
})(
//disabled={dataSource.id > 0}
<Input placeholder='Name' />
)}
</Form.Item>
<Form.Item label='Status' hasFeedback>
{getFieldDecorator('status', {
initialValue: dataSource.status,
rules: [{
required: true,
message: 'Please enter the Status'
}],
})(
<Select>
<Select.Option value="Active">Active</Select.Option>
<Select.Option value="InActive">InActive</Select.Option>
</Select>
)}
</Form.Item>
<Form.Item label='Type' hasFeedback>
{getFieldDecorator('type', {
initialValue: dataSource.type || 'Fixed Single',
rules: [{
required: true,
message: 'Please select the Type'
}],
})(
<Select onChange={handleTypeChanged}>
<Select.Option value="Fixed Single">Fixed Single</Select.Option>
<Select.Option value="Fixed Multiple">Fixed Multiple</Select.Option>
<Select.Option value="Dynamic">Dynamic</Select.Option>
</Select>
)}
</Form.Item>
<Form.Item label='Total Days' hasFeedback style={selectedType === 'Dynamic' ? { display: '' } : { display: 'none' }}>
{getFieldDecorator('totaldays', {
initialValue: dataSource.totalDays,
rules: [{
required: isTotalDaysRequired,
message: isTotalDaysRequiredMessage
}],
})(
<InputNumber min={1} max={365} />
)}
</Form.Item>
<ShiftDays inputList={inputList} setInputList={setInputList} selectedType={selectedType} handleDaysValueChange={handleDaysValueChange} getFieldDecorator={getFieldDecorator} />
</Form>
</div>
)};
const onFieldsChange = (props, changedFields, allFields) => {
if (props.onFieldsChange) {
props.onFieldsChange(props, changedFields, allFields);
}
};
const onValuesChange = (props, changedValues, allValues) => {
if (props.onValuesChange) {
props.onValuesChange(props, changedValues, allValues);
}
};
ShiftRosterFormDetails.propTypes = {
form: PropTypes.object,
dataSource: PropTypes.object,
onFieldsChange: PropTypes.func,
onValuesChange: PropTypes.func
};
ShiftRosterFormDetails.defaultProps = {
form: {},
dataSource: {},
onFieldsChange() { },
onValuesChange() { }
};
export default Form.create({
onValuesChange,
onFieldsChange
})(ShiftRosterFormDetails);
Got the pictured error while trying to set up onChange event handler for the FormRow function. Ultimately need to store the user data from Que Reset Time, Service Level threshold and Short Abandoned Threshold. The Queuecontainer is the main class that uses the editor to create the container forms.
QuesContainer.js
import React from 'react';
import Editor from './Editor';
import LoginForm from '../LoginForm';
import axios from 'axios';
export default class QueuesContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
workspaceSID: '',
apiKey: '',
apiSecret: '',
queues: [],
name: '',
}
this.listQueues = this.listQueues.bind(this);
this.setCredentials = this.setCredentials.bind(this);
this.getCredentials = this.getCredentials.bind(this);
this.setQueues = this.setQueues.bind(this);
this.handleChange = this.handleChange.bind(this);
}
/*
handleChange(target) {
this.setState({ [target.id]: target.value });
}
*/
//Added by JR for Save Function
handleChange(event){
console.log("From handle change", event);
this.setState({ name: event.target.value })
}
/*
handleSubmit(event) {
event.preventDefault();
this.props.login(this.state.workspaceSID, this.state.apiKey, this.state.apiSecret);
}
*/
handleSubmit(event) {
event.preventDefault();
var text = this.state.text;
console.log("form testing value output:");
}
setCredentials(workspaceSID, apiKey, apiSecret) {
this.setState({ workspaceSID: workspaceSID });
this.setState({ apiKey: apiKey });
this.setState({ apiSecret: apiSecret });
}
getCredentials() {
return this.state;
}
setQueues(queues) {
this.setState({ queues: queues });
}
getConfig(apiKey, apiSecret) {
return axios.get(`https://flex-api.twilio.com/v1/Configuration`, {
auth: {
username: apiKey,
password: apiSecret
},
});
}
//Added by JR used to post configuration
postConfig(apiKey, apiSecret, params) {
return axios.post(`https://flex-api.twilio.com/v1/Configuration`, {
auth: {
username: apiKey,
password: apiSecret
},
});
}
listQueues(workspaceSID, apiKey, apiSecret) {
this.setCredentials(workspaceSID, apiKey, apiSecret);
const queuesPromise = axios.get(`https://taskrouter.twilio.com/v1/Workspaces/${workspaceSID}/TaskQueues?PageSize=1000&Page=0`, {
auth: {
username: apiKey,
password: apiSecret
},
});
const channelsPromise = axios.get(`https://taskrouter.twilio.com/v1/Workspaces/${workspaceSID}/TaskChannels?PageSize=1000&Page=0`, {
auth: {
username: apiKey,
password: apiSecret
},
});
const configPromise = this.getConfig(apiKey, apiSecret);
Promise.all([queuesPromise, channelsPromise, configPromise])
.then((values) => {
console.log(values);
const twilioQueues = values[0].data.task_queues;
const twilioChannels = values[1].data.channels;
// const config = values[2].data.queue_stats_configuration;
const voice = twilioChannels.find(channel => {
console.log(channel)
return channel.unique_name === 'voice'
});
const chat = twilioChannels.find(channel => {
console.log(channel)
return channel.unique_name === 'chat'
});
const email = twilioChannels.find(channel => {
console.log(channel)
return channel.unique_name === 'email'
});
const channels = [voice, chat, email];
const queues = twilioQueues.map(q => {
const queue = {
queue_sid: q.sid,
friendly_name: q.friendly_name,
channels
}
return queue;
});
this.setQueues(queues);
});
}
//onChange={this.handleInputChange.bind(this)}
render() {
const { name } = this.state
return (
<div>
<p> Test Value is : {name} </p>
<LoginForm login={this.listQueues} buttonText='Load Queues' />
<Editor onSubmit={this.handleSubmit} onChange={this.handleChange} data={this.state.queues} credentials={this.getCredentials} setData={this.setQueues} />
</div>
);
}
}
Editor.js
import React from 'react';
import MaterialTable from 'material-table'
import Button from '#material-ui/core/Button';
import TextField from '#material-ui/core/TextField';
import Grid from '#material-ui/core/Grid';
import axios from 'axios';
import { waitForDomChange } from '#testing-library/react';
export default class Editor extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
queue: {},
columns: [
{ title: 'Queue Name', field: 'friendly_name', editable: 'never' },
{ title: 'Channel', field: 'channel_friendly_name', editable: 'never' },
{ title: 'Reset Timezone', field: 'channel_reset_timezone' },
{ title: 'Reset Time', field: 'channel_reset_time' },
{ title: 'Service Level Threshold', field: 'channel_service_level_threshold', type: 'numeric' },
{ title: 'Short Abandoned Threshold', field: 'channel_service_level_threshold', type: 'numeric' },
]
}
this.handleClose = this.handleClose.bind(this);
// this.handleSave = this.handleSave.bind(this);
}
handleClose(xxx) {
console.log('this happening');
console.log(xxx);
this.setState({ open: true });
}
/*
handleSave(event) {
console.log('Saving user input data');
console.log(event);
this.setState({ value: event.target.value});
}
*/
render() {
console.log('hey ho');
console.group(this.props.data);
const queues = this.props.data.map(q => {
const channel = q.channels.find(c => c.unique_name === 'voice');
q.channel_friendly_name = channel.friendly_name;
q.channel_reset_time = channel.reset_time;
q.channel_reset_timezone = channel.reset_timezone;
q.channel_service_level_threshold = channel.service_level_threshold;
q.channel_service_level_threshold = channel.service_level_threshold;
return q;
})
return (
<div>
<MaterialTable
title='Queue SLA'
columns={this.state.columns}
data={queues}
options={{
pageSize: 25,
pageSizeOptions: [25, 100, 500, 1000]
}}
detailPanel={rowData => {
//Modified by JR for Save Function
return FormContainer(rowData, this.props.credentials().apiKey,
this.props.credentials().apiSecret, this.handleChange);
}}
/>
</div>
);
}
}
function FormRow(data, handleChange) {
const queue = data.data;
const channels = queue.channels;
return (
channels.map((channel, index) => (
<Grid container item sm={12} spacing={3}>
<Grid item sm={2}>
<TextField
autoFocus
margin="dense"
id="channel_name"
label="Channel"
type="text"
value={channel.friendly_name}
fullWidth
disabled
/>
</Grid>
<Grid item sm={2}>
<TextField
autoFocus
margin="dense"
id="reset_timezone"
label="Reset Timezone"
type="text"
value="GMT"
fullWidth
disabled
/>
</Grid>
<Grid item sm={2}>
<TextField
autoFocus
margin="dense"
id="service_level_threshold"
label="Service Level Threshold"
value={channel.service_level_threshold}
onChange={handleChange}
type="text"
fullWidth
/>
</Grid>
<Grid item sm={2}>
<TextField
autoFocus
margin="dense"
id="short_abandoned_threshold"
label="Short Abandoned Threshold"
value={channel.short_abandoned_threshold}
onChange={handleChange}
type="text"
fullWidth
/>
</Grid>
</Grid>
))
);
}
//Modified by JR for Save Function
function FormContainer(data, apiKey, apiSecret, props, handleChange) {
console.log('what');
console.log(data);
console.log('JSON what');
console.log(JSON.stringify(data));
const queue = data;
// const queue = data.data;
const channels = queue.channels;
const cancel = () => {
console.log('cancel');
}
const save = () => {
//Modified by JR for Save Function
console.log('save');
console.log(data);
console.log(JSON.stringify(data));
console.log('SaveButtonClicked');
waitForDomChange();
console.log(data);
console.log(JSON.stringify(data));
savebuttonclicked(queue, apiKey, apiSecret);
console.log('Props Information');
console.log(props);
console.log('Save Sucessful');
}
return (
<div>
<Grid container>
<Grid item sm={1}></Grid>
<Grid container item sm={11} spacing={3}>
<FormRow data={queue}
formOnChange={handleChange}/>
</Grid>
<Grid
container
direction="row"
justify="flex-end"
alignItems="center"
>
<Grid item sm={1}></Grid>
<Grid item sm={11} spacing={3} justify="flex-end"
alignItems="center">
<Grid item sm={2}>
<TextField
autoFocus
margin="dense"
id="reset_time"
label="Queue Reset Time"
type="text"
value={channels.reset_time}
onChange={handleChange}
fullWidth
/>
</Grid>
<Button variant="outlined" onClick={cancel} color="secondary">Cancel</Button>
<Button variant="outlined" onClick={save} color="primary">Save</Button>
</Grid>
</Grid>
</Grid>
</div>
);
}
//Add by JR for Save Function
function savebuttonclicked(data, apiKey, apiSecret) {
const workspace = workspaces[0];
console.log('Test');
alert(JSON.stringify(data));
console.log(JSON.stringify(data));
var params = [];
for (const [index, value] of data.channels.entries()) {
params.push({
'url': data.channels[index].url,
'unique_name': data.channels[index].unique_name,
'account_sid': data.channels[index].account_sid,
'channel_reset_time': data.channels[index].channel_reset_time,
'channel_reset_timezone': data.channels[index].channel_reset_timezone,
'channel_service_level_threshold': data.channels[index].channel_service_level_threshold
})
}
alert(JSON.stringify(params));
console.log('Parms for API post:');
console.log(JSON.stringify(params));
/*
* Loop for API call for each URL in created JSON
*
for (const [index, value] of params.entries())
{
axios.post(params[index].url, params[index], {
auth: {
username: apiKey,
password: apiSecret,
}
})
}
*/
axios.get(workspace.apiURL, {
auth: {
username: apiKey,
password: apiSecret,
},
})
.then(function (response) {
alert('Save Sucessful.')
alert(JSON.stringify(response))
console.log('success');
console.log(response.headers);
})
.catch(function (error) {
alert('Error Occured.')
alert(error)
console.log('Error');
console.log(error);
});
}
With applied fix this is new error:
TypeError: Cannot read property 'channels' of undefined FormRow
C:/Users/drago/Source/Repos/twilio-flex-editor/src/queues/Editor.js:99
96 | 97 | function FormRow({ data, handleChange }) { 98 | const queue
= data.data; > 99 | const channels = queue.channels; 100 | 101 | 102 | View compiled ▶ 18 stack frames were collapsed.
Issue
You have defined FormRow to consume a handleChange callback and also incorrectly defined the props. They should be destructured from a single props object.
function FormRow(data, handleChange) {...
but have passed an formOnChange prop callback
<FormRow data={queue} formOnChange={handleChange} />
You also pass this.handleChange to Editor but never consume it
<Editor
onSubmit={this.handleSubmit}
onChange={this.handleChange}
data={this.state.queues}
credentials={this.getCredentials}
setData={this.setQueues}
/>
and (possibly coincidentally) pass an undefined this.handleChange function on to FormContainer which is passed to FormRow component.
Solution
Fix the FormRow component definition
function FormRow({ data, handleChange }) {...
Pass handleChange on to FormRow as handleChange.
<FormRow data={queue} handleChange={handleChange} />
Either define this.handleChange in Editor to be passed, or pass this.props.onChange on.
return FormContainer(
rowData,
this.props.credentials().apiKey,
this.props.credentials().apiSecret,
this.props.onChange,
);
This is a picture to the error i received:
enter image description here
I'm working on the Pro MERN Stack: Full Stack Web App Development with Mongo, Express, React, and Node book, and I ran into CORS policy errors whenever I tried to go back to my homepage. It looks like the error is coming from my graphqlFetch file but I'm not so sure what the issue is. Any insight would be extremely helpful!!
CORS policy error message
GraphQLFetch error
1:
My GraphQLFetch code:
import fetch from 'isomorphic-fetch';
const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d');
function jsonDateReviver(key, value) {
if (dateRegex.test(value)) return new Date(value);
return value;
}
export default async function
graphQLFetch(query, variables = {}, showError = null, cookie = null) {
const apiEndpoint = (__isBrowser__) // eslint-disable-line no-undef
? window.ENV.UI_API_ENDPOINT
: process.env.UI_SERVER_API_ENDPOINT;
try {
const headers = { 'Content-Type': 'application/json' };
if (cookie) headers.Cookie = cookie;
const response = await fetch(apiEndpoint, {
method: 'POST',
credentials: 'include',
headers,
body: JSON.stringify({ query, variables }),
});
const body = await response.text();
const result = JSON.parse(body, jsonDateReviver);
if (result.errors) {
const error = result.errors[0];
if (error.extensions.code === 'BAD_USER_INPUT') {
const details = error.extensions.exception.errors.join('\n ');
if (showError) showError(`${error.message}:\n ${details}`);
} else if (showError) {
showError(`${error.extensions.code}: ${error.message}`);
}
}
return result.data;
} catch (e) {
if (showError) showError(`Error in sending data to server: ${e.message}`);
return null;
}
}
My IssueEdit file
import React from 'react';
import { Link } from 'react-router-dom';
import { LinkContainer } from 'react-router-bootstrap';
import {
Col, Panel, Form, FormGroup, FormControl, ControlLabel,
ButtonToolbar, Button, Alert,
} from 'react-bootstrap';
import graphQLFetch from './graphQLFetch.js';
import NumInput from './NumInput.jsx';
import DateInput from './DateInput.jsx';
import TextInput from './TextInput.jsx';
import Toast from './Toast.jsx';
import store from './store.js';
export default class IssueEdit extends React.Component {
static async fetchData(match, search, showError) {
const query = `query issue($id: Int!) {
issue(id: $id) {
id title status owner
effort created due description
}
}`;
const { params: { id } } = match;
const result = await graphQLFetch(query, { id }, showError);
return result;
}
constructor() {
super();
const issue = store.initialData ? store.initialData.issue : null;
delete store.initialData;
this.state = {
issue,
invalidFields: {},
showingValidation: false,
toastVisible: false,
toastMessage: '',
toastType: 'success',
};
this.onChange = this.onChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.onValidityChange = this.onValidityChange.bind(this);
this.dismissValidation = this.dismissValidation.bind(this);
this.showValidation = this.showValidation.bind(this);
this.showSuccess = this.showSuccess.bind(this);
this.showError = this.showError.bind(this);
this.dismissToast = this.dismissToast.bind(this);
}
componentDidMount() {
const { issue } = this.state;
if (issue == null) this.loadData();
}
componentDidUpdate(prevProps) {
const { match: { params: { id: prevId } } } = prevProps;
const { match: { params: { id } } } = this.props;
if (id !== prevId) {
this.loadData();
}
}
onChange(event, naturalValue) {
const { name, value: textValue } = event.target;
const value = naturalValue === undefined ? textValue : naturalValue;
this.setState(prevState => ({
issue: { ...prevState.issue, [name]: value },
}));
}
onValidityChange(event, valid) {
const { name } = event.target;
this.setState((prevState) => {
const invalidFields = { ...prevState.invalidFields, [name]: !valid };
if (valid) delete invalidFields[name];
return { invalidFields };
});
}
async handleSubmit(e) {
e.preventDefault();
this.showValidation();
const { issue, invalidFields } = this.state;
if (Object.keys(invalidFields).length !== 0) return;
const query = `mutation issueUpdate(
$id: Int!
$changes: IssueUpdateInputs!
) {
issueUpdate(
id: $id
changes: $changes
) {
id title status owner
effort created due description
}
}`;
const { id, created, ...changes } = issue;
const data = await graphQLFetch(query, { changes, id }, this.showError);
if (data) {
this.setState({ issue: data.issueUpdate });
this.showSuccess('Updated issue successfully');
}
}
async loadData() {
const { match } = this.props;
const data = await IssueEdit.fetchData(match, null, this.showError);
this.setState({ issue: data ? data.issue : {}, invalidFields: {} });
}
showValidation() {
this.setState({ showingValidation: true });
}
dismissValidation() {
this.setState({ showingValidation: false });
}
showSuccess(message) {
this.setState({
toastVisible: true, toastMessage: message, toastType: 'success',
});
}
showError(message) {
this.setState({
toastVisible: true, toastMessage: message, toastType: 'danger',
});
}
dismissToast() {
this.setState({ toastVisible: false });
}
render() {
const { issue } = this.state;
if (issue == null) return null;
const { issue: { id } } = this.state;
const { match: { params: { id: propsId } } } = this.props;
if (id == null) {
if (propsId != null) {
return <h3>{`Issue with ID ${propsId} not found.`}</h3>;
}
return null;
}
const { invalidFields, showingValidation } = this.state;
let validationMessage;
if (Object.keys(invalidFields).length !== 0 && showingValidation) {
validationMessage = (
<Alert bsStyle="danger" onDismiss={this.dismissValidation}>
Please correct invalid fields before submitting.
</Alert>
);
}
const { issue: { title, status } } = this.state;
const { issue: { owner, effort, description } } = this.state;
const { issue: { created, due } } = this.state;
const { toastVisible, toastMessage, toastType } = this.state;
return (
<Panel>
<Panel.Heading>
<Panel.Title>{`Editing issue: ${id}`}</Panel.Title>
</Panel.Heading>
<Panel.Body>
<Form horizontal onSubmit={this.handleSubmit}>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Created</Col>
<Col sm={9}>
<FormControl.Static>
{created.toDateString()}
</FormControl.Static>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Status</Col>
<Col sm={9}>
<FormControl
componentClass="select"
name="status"
value={status}
onChange={this.onChange}
>
<option value="New">New</option>
<option value="Assigned">Assigned</option>
<option value="Fixed">Fixed</option>
<option value="Closed">Closed</option>
</FormControl>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Owner</Col>
<Col sm={9}>
<FormControl
componentClass={TextInput}
name="owner"
value={owner}
onChange={this.onChange}
key={id}
/>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Effort</Col>
<Col sm={9}>
<FormControl
componentClass={NumInput}
name="effort"
value={effort}
onChange={this.onChange}
key={id}
/>
</Col>
</FormGroup>
<FormGroup validationState={
invalidFields.due ? 'error' : null
}
>
<Col componentClass={ControlLabel} sm={3}>Due</Col>
<Col sm={9}>
<FormControl
componentClass={DateInput}
onValidityChange={this.onValidityChange}
name="due"
value={due}
onChange={this.onChange}
key={id}
/>
<FormControl.Feedback />
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Title</Col>
<Col sm={9}>
<FormControl
componentClass={TextInput}
size={50}
name="title"
value={title}
onChange={this.onChange}
key={id}
/>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Description</Col>
<Col sm={9}>
<FormControl
componentClass={TextInput}
tag="textarea"
rows={4}
cols={50}
name="description"
value={description}
onChange={this.onChange}
key={id}
/>
</Col>
</FormGroup>
<FormGroup>
<Col smOffset={3} sm={6}>
<ButtonToolbar>
<Button bsStyle="primary" type="submit">Submit</Button>
<LinkContainer to="/issues">
<Button bsStyle="link">Back</Button>
</LinkContainer>
</ButtonToolbar>
</Col>
</FormGroup>
<FormGroup>
<Col smOffset={3} sm={9}>{validationMessage}</Col>
</FormGroup>
</Form>
</Panel.Body>
<Panel.Footer>
<Link to={`/edit/${id - 1}`}>Prev</Link>
{' | '}
<Link to={`/edit/${id + 1}`}>Next</Link>
</Panel.Footer>
<Toast
showing={toastVisible}
onDismiss={this.dismissToast}
bsStyle={toastType}
>
{toastMessage}
</Toast>
</Panel>
);
}
}
My IssueList file:
import React from 'react';
import URLSearchParams from 'url-search-params';
import { Panel } from 'react-bootstrap';
import IssueFilter from './IssueFilter.jsx';
import IssueTable from './IssueTable.jsx';
import IssueDetail from './IssueDetail.jsx';
import graphQLFetch from './graphQLFetch.js';
import withToast from './withToast.jsx';
import store from './store.js';
class IssueList extends React.Component {
static async fetchData(match, search, showError) {
const params = new URLSearchParams(search);
const vars = { hasSelection: false, selectedId: 0 };
if (params.get('status')) vars.status = params.get('status');
const effortMin = parseInt(params.get('effortMin'), 10);
if (!Number.isNaN(effortMin)) vars.effortMin = effortMin;
const effortMax = parseInt(params.get('effortMax'), 10);
if (!Number.isNaN(effortMax)) vars.effortMax = effortMax;
const { params: { id } } = match;
const idInt = parseInt(id, 10);
if (!Number.isNaN(idInt)) {
vars.hasSelection = true;
vars.selectedId = idInt;
}
const query = `query issueList(
$status: StatusType
$effortMin: Int
$effortMax: Int
$hasSelection: Boolean!
$selectedId: Int!
) {
issueList(
status: $status
effortMin: $effortMin
effortMax: $effortMax
) {
id title status owner
created effort due
}
issue(id: $selectedId) #include (if : $hasSelection) {
id description
}
}`;
const data = await graphQLFetch(query, vars, showError);
return data;
}
constructor() {
super();
const issues = store.initialData ? store.initialData.issueList : null;
const selectedIssue = store.initialData
? store.initialData.issue
: null;
delete store.initialData;
this.state = {
issues,
selectedIssue,
};
this.closeIssue = this.closeIssue.bind(this);
this.deleteIssue = this.deleteIssue.bind(this);
}
componentDidMount() {
const { issues } = this.state;
if (issues == null) this.loadData();
}
componentDidUpdate(prevProps) {
const {
location: { search: prevSearch },
match: { params: { id: prevId } },
} = prevProps;
const { location: { search }, match: { params: { id } } } = this.props;
if (prevSearch !== search || prevId !== id) {
this.loadData();
}
}
async loadData() {
const { location: { search }, match, showError } = this.props;
const data = await IssueList.fetchData(match, search, showError);
if (data) {
this.setState({ issues: data.issueList, selectedIssue: data.issue });
}
}
async closeIssue(index) {
const query = `mutation issueClose($id: Int!) {
issueUpdate(id: $id, changes: { status: Closed }) {
id title status owner
effort created due description
}
}`;
const { issues } = this.state;
const { showError } = this.props;
const data = await graphQLFetch(query, { id: issues[index].id },
showError);
if (data) {
this.setState((prevState) => {
const newList = [...prevState.issues];
newList[index] = data.issueUpdate;
return { issues: newList };
});
} else {
this.loadData();
}
}
async deleteIssue(index) {
const query = `mutation issueDelete($id: Int!) {
issueDelete(id: $id)
}`;
const { issues } = this.state;
const { location: { pathname, search }, history } = this.props;
const { id } = issues[index];
const { showSuccess, showError } = this.props;
const data = await graphQLFetch(query, { id }, showError);
if (data && data.issueDelete) {
this.setState((prevState) => {
const newList = [...prevState.issues];
if (pathname === `/issues/${id}`) {
history.push({ pathname: '/issues', search });
}
newList.splice(index, 1);
return { issues: newList };
});
showSuccess(`Deleted issue ${id} successfully.`);
} else {
this.loadData();
}
}
render() {
const { issues } = this.state;
if (issues == null) return null;
const { selectedIssue } = this.state;
return (
<React.Fragment>
<Panel>
<Panel.Heading>
<Panel.Title toggle>Filter</Panel.Title>
</Panel.Heading>
<Panel.Body collapsible>
<IssueFilter />
</Panel.Body>
</Panel>
<IssueTable
issues={issues}
closeIssue={this.closeIssue}
deleteIssue={this.deleteIssue}
/>
<IssueDetail issue={selectedIssue} />
</React.Fragment>
);
}
}
const IssueListWithToast = withToast(IssueList);
IssueListWithToast.fetchData = IssueList.fetchData;
export default IssueListWithToast;
You have to allow cross origin on your graphql server. For security reasons browsers didn't allow ajax request on cross origin. So lets say you're on localhost:3000 and your server is running on localhost:8000 so these two are on different origins.
So use cors module on Express server and allow Access-Control-Allow-Origin to "*"
Long story short, I have a table where I've build a searching functionality using the following code to search in the object arrays and update the state using hooks.
const SearchView = (e) => {
e.preventDefault();
console.log(e.target.value)
if (!e.target.value) {
setSearchDataSuggestions([])
setSearchField(null)
} else {
setSearchField(true)
props.setSeachingView(true)
for (const row of Object.values(rows)) {
Object.keys(row).forEach(function (item, index) {
if (typeof(row[item]) === 'string' && row[item].toLowerCase().includes(e.target.value.toLowerCase().trim())) {
setSearchDataSuggestions([...search_data_suggestions, (rows[index]) ])
}
});
}
}
}
The code works as fine in the npm start but wont work at production build ... Whenever I try to type something into the text field (where the SearchView() is called onChange) it crashes on a blank page along the following console error:
Console Error
I've no idea where this error is telling me ... Since there's no .id being passed whatsoever in my code. By any chance, I'm posting the entire code here anyways.
Can someone please help me identify where is the bug trigger and why it works fine in a dev server and not in prod !??!
Full code goes below:
import React, { useState, useEffect } from 'react';
import DataTable from 'react-data-table-component';
import moment from 'moment'
import TextField from '#material-ui/core/TextField';
import InputAdornment from '#material-ui/core/InputAdornment';
const LookUpTable = (props) => {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [rows, setItems] = useState([]);
const [search_data_suggestions, setSearchDataSuggestions] = useState([]);
const [accessToken] = useState(props.accessToken)
const [isSearching] = useState(props.isSearching);
const [search_field, setSearchField] = useState(null);
const success_validation = row_success => {
if (row_success) {
return "True";
} else {
return "False";
}
};
const columns = [
{
name: 'Timestamp',
selector: 'timestamp',
sortable: true,
format: row => moment(row.timestamp).format('lll'),
},
{
name: 'Project ID',
selector: 'project_id',
sortable: true,
},
{
name: 'Client Name',
selector: 'client_name',
sortable: true,
right: true,
},
{
name: 'Client Email',
selector: 'client_email',
sortable: true,
right: true,
},
{
name: 'Client Mobile',
selector: 'client_mobile',
sortable: true,
},
{
name: 'Sent By',
selector: 'sent_by',
sortable: true,
},
{
name: 'Success',
selector: 'success_validation',
sortable: true,
right: true,
format: row => success_validation(row.success),
},
];
const ExpanableComponent = ({ data }) => {
return(
<div>
<p></p>
<ul>
<li><strong>Report Filename: </strong>{data.file_name} </li>
<li><strong>Project Description: </strong>{data.project_desc} </li>
<li><strong>Project Owner's Name: </strong>{data.project_owner_name} </li>
<li><strong>Project Owner's Email: </strong>{data.project_owner_email} </li>
</ul>
</div>
)
}
const SearchView = (e) => {
e.preventDefault();
console.log(e.target.value)
// setSearchValue(e.target.value.toLowerCase())
// const values = (Object.values(rows));
if (!e.target.value) {
setSearchDataSuggestions([])
setSearchField(null)
} else {
setSearchField(true)
props.setSeachingView(true)
for (const row of Object.values(rows)) {
Object.keys(row).forEach(function (item, index) {
// console.log('[' + index + '] ' + 'keys: ' + item + ' values : ' + row[item]);
if (typeof(row[item]) === 'string' && row[item].toLowerCase().includes(e.target.value.toLowerCase().trim())) {
setSearchDataSuggestions([...search_data_suggestions, (rows[index])])
// console.log('Got search as: ' + e.target.value + ' and updated with the row: ' + (rows[index]) )
}
});
}
}
}
const CustonSearchViewRender = () => {
if (search_field) {
return (
<DataTable
columns={columns}
data={search_data_suggestions}
highlightOnHover={true}
pagination={true}
fixedHeader={true}
expandableRows={true}
expandableRowsComponent={<ExpanableComponent />}
striped={true}
/>
)
}
}
// Note: the empty deps array [] means
// this useEffect will run once
// similar to componentDidMount()
useEffect(() => {
fetch("/lookup/", {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
}
}
)
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setItems(result);
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, [accessToken])
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<div className='lookup-table container-fluid'>
<div className="search-bar container">
<br></br>
<h4 className="search-bar-title">Past 90 days report table lookup ...</h4>
<TextField
label="Search me"
onChange={e => SearchView(e)}
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<i className="fas fa-search"></i>
</InputAdornment>
)
}}
/>
</div>
{isSearching && !search_field?
<DataTable
columns={columns}
data={rows}
highlightOnHover={true}
pagination={true}
fixedHeader={true}
expandableRows={true}
expandableRowsComponent={<ExpanableComponent />}
striped={true}
/> : null}
{search_field ? <CustonSearchViewRender/> : null}
</div>
);
}
}
export default LookUpTable;
Thank you so much.