Update task attributes in flex for assigned task - twilio

I am updating a task property through a request to my backend for an assigned task, this request is made on a custom component, on a button click during the call.
I can see in my twilio console that the update went fine and the new value is shown there, but the task prop on my flex components are not being updated, maintaining the same attributes since the assignment.
Is there a way to "refresh" the task in flex? I would need this updated attribute in order to perform a conditional rendering on my custom component.
Thanks in advance to anyone that helps me out.
import React from 'react';
import { withTaskContext } from '#twilio/flex-ui';
class IsRecording extends React.Component {
constructor(props) {
super(props);
}
render() {
// this.task = this.props.tasks
return (
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#E50000">
<path
d="M16 16c0 1.104-.896 2-2 2h-12c-1.104 0-2-.896-2-2v-8c0-1.104.896-2 2-2h12c1.104 0 2 .896 2 2v8zm8-10l-6 4.223v3.554l6 4.223v-12z"/>
<animate
attributeType="XML"
attributeName="fill"
values="#800;#f00;#800;#800"
dur="1.5s"
repeatCount="indefinite"/>
</svg>
</div>
)
}
};
export default withTaskContext(IsRecording);

Twilio developer evangelist here.
If you have access to the task object within Flex you do not need to do so via the back-end. Instead, you can call on task.setAttributes and that will update the task attributes directly and cause it to update the state everywhere within Flex.
However, the difficulty here is that a supervisor will not have a live view on tasks, so you need to do a little more work to have the supervisor listen for changes on the tasks. I had a play around and got this working. It uses the Sync liveQuery interface to subscribe to updates to workers' tasks.
We create the live query subscription on componentDidMount. The first argument when creating the live query is "tr-task" and this refers to all tasks in the system. The second argument is the query, in this case we are querying for all tasks for this worker.
Once we get the query we can load the current items and set them in the state. We then listen for additions, updates and removals and update the state accordingly.
In the render function we calculate whether any of the tasks have an isRecording attribute and display the icon accordingly.
Here's the <IsRecording> component:
import React from "react";
import { Manager } from "#twilio/flex-ui";
class IsRecording extends React.Component {
constructor(props) {
super(props);
this.state = {
tasks: [],
};
}
componentDidMount() {
Manager.getInstance()
.insightsClient.liveQuery(
"tr-task",
`data.worker_sid == "${this.props.worker.sid}"`
)
.then((query) => {
this.liveQuery = query;
const items = query.getItems();
this.setState({ tasks: Object.values(items) });
query.on("itemAdded", (item) => {
this.setState((prevState) => ({
tasks: [...prevState.tasks, item.value],
}));
});
query.on("itemUpdated", (item) => {
this.setState((prevState) => ({
tasks: [
...prevState.tasks.filter((task) => task.sid !== item.value.sid),
item.value,
],
}));
});
query.on("itemRemoved", (item) => {
this.setState((prevState) => ({
tasks: prevState.tasks.filter(
(task) => task.sid !== item.previousItemData.sid
),
}));
});
})
.catch((err) =>
console.debug(`Error when subscribing to live updates for Tasks`, err)
);
}
componentWillUnmount() {
if (this.liveQuery) {
this.liveQuery.removeAllListeners();
this.liveQuery.close();
}
}
render() {
if (this.state.tasks.some((task) => task.attributes.isRecording)) {
return (
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#E50000">
<path d="M16 16c0 1.104-.896 2-2 2h-12c-1.104 0-2-.896-2-2v-8c0-1.104.896-2 2-2h12c1.104 0 2 .896 2 2v8zm8-10l-6 4.223v3.554l6 4.223v-12z" />
<animate attributeType="XML" attributeName="fill" values="#800;#f00;#800;#800" dur="1.5s" repeatCount="indefinite" />
</svg>
</div>
);
} else {
return null;
}
}
}
export default IsRecording;
You attach it to the WorkersDataTable like this:
flex.WorkersDataTable.Content.add(
<ColumnDefinition
key="is-recording"
header={"Recording"}
content={(items) => {
return (
<IsRecording
key={`is-recording-${items.worker.sid}`}
worker={items.worker}
/>
);
}}
/>
);

I'm accomplishing this through reading from the redux store. Below has been modified so I don't reveal proprietary knowledge.
Create a custom listener for the Twilio reservationCreated event
manager.workerClient.on("reservationCreated", this.acceptTask);
The below fetches the updated task. If we used task directly it would be the stale object unless things have changed.
async acceptTask(reservation) {
const updatedTask = this.manager.store.getState().flex.worker.tasks.get(reservation.task.sid);
// consume it
// get the updated attributes with updatedTask.attributes
}
Hope that works for you / helps.

Related

Old component retains state on back button

I have a link in a modal that goes to a new page, and I can't seem to reset the state of the component with the modal when directing to that page.
The component with the modal seems to be keeping its state after directing to the new page, because when I hit the back button, it automatically opens the modal.
The modal is either opened or closed based on the state of modalIsOpen.
So I have my simplified Listings component:
import React from 'react'
import ListingModalContent from '../ListingModalContent'
import Modal from '../Modal'
export default class Listings extends React.Component {
constructor(props) {
super(props)
this.state = {
modalIsOpen: false,
modalContent: null
}
}
modalClick = (e, listing) => {
e.preventDefault()
this.setState({
modalContent: <ListingModalContent listing={listing}/>
}, () => {
this.setState({modalIsOpen: true})
})
}
modalClose = () => {
this.setState({modalIsOpen: false})
}
componentWillMount() {
this.setState({modalIsOpen: false})
console.log('mounting...')
console.log(this.state.modalIsOpen)
}
componentWillUnmount() {
console.log('unmounting...')
this.setState({
modalIsOpen: false
}, () => {
console.log('got here...')
console.log(this.state.modalIsOpen)
})
console.log(this.state.modalIsOpen)
}
render() {
const listings = this.props.listings.map(listing => (<div className="listing">
<a href="#" onClick={e => this.modalClick(e, listing)}>More Details</a>
</div>))
return (<div id="listings">
<section className="listings">
{listings}
<Modal visible={this.state.modalIsOpen} onClose={this.modalClose}>
{this.state.modalContent}
</Modal>
</section>
</div>)
}
}
And my ListingsModalContent component:
import React from 'react'
export default class ListingModalContent extends React.Component {
constructor(props) {
super(props)
}
render() {
const {listing} = this.props
return (<div className="listing-modal">
<div className="details">
<h2 className="address">{listing.address}</h2>
<p className="description">{listing.description}</p>
</div>
<div className="btn-container">
<a href={`/listings/${listing.slug}`} onClick={this.props.modalClose}>View Full Listing</a>
</div>
</div>)
}
}
The console output is...
// after initially mounting:
mounting...
false
// after clicking the listing link:
unmounting...
true
// after hitting the back button:
mounting...
false
I'm pretty sure I need to fix this by using componentWillUnmount to set the state of modalIsOpen to false before the component unmounts, but it never seems to finish setting the state before unmounting.
I'm using react on rails, which seems to use some hybrid routing rails/react routing system, but I'm not too familiar with it, and don't want to go down that rabbit hole at the moment if I don't have to.
So my question is, if this is expected behavior of the react component lifecycle, is there a way I can ensure the state of modalIsOpen is reset before unmounting? Or is there a way I can make sure my state is reset to its initial state when going back? Or is this more likely a consequence of the routing system I'm using?
This is strange, unexpected bahaviour in react and for sure is not caused by react (as #azium stated) but some 'things around', probably react_on_rails issue (or 'feature'). Report a bug/create an issue on github.
As you see in log state has proper value on mounting and there is no reason to render modal. 'Normal' react would work as expected.
There is no sense to set state on unmount - instance of component will be destroyed, its state, too.
HINTS
You shouldn't store modal content in state. It's possible, it works for simple cases, it can be used a kind of cache for parts of content, but you can have issues when conditional rerendering needed (using prop/state changes).
After setting state this.setState({modalIsOpen: true, modalContent:listing}) in click handler you can use conditional rendering (in render):
{this.state.modalIsOpen && <ListingModalContent listing={this.state.modalContent}/>}
To be true even this.setState({modalIsOpen: true}) can be removed (by save only listing idx in state, '-1' for closing) but then code can be less readable (storing additional pointer is cheap).

You have included the Google Maps JavaScript API multiple times on this page

how can I avoid “You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.” if I am using google-map-react to display the map and react-places-autocomplete in another component to get the address and coordinates ?
//LocationMapPage component that displays the map box and pass the props to it
class LocationMapPage extends Component {
render() {
let {latLng,name,address} = this.props.location;
return (
<MapBox lat={latLng.lat} lng={latLng.lng} name={name} address={address}/>
)
}
}
//MapBox component
import React from "react";
import GoogleMapReact from 'google-map-react';
import apiKey from "../../configureMap";
const Marker = () => <i className="fa fa-map-marker fa-2x text-danger" />
const MapBox = ({lat,lng, name, address}) => {
const center = [lat,lng];
const zoom = 14;
return (
<div style={{ height: '300px', width: '100%' }}>
<GoogleMapReact
bootstrapURLKeys={{ key: apiKey }}
defaultCenter={center}
defaultZoom={zoom}
>
<Marker
lat={lat}
lng={lng}
text={`${name}, ${address}`}
/>
</GoogleMapReact>
</div>
);
}
export default MapBox;
Map is blank:
The Error in the console:You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.
How to solve?
I am using google-map-react, react-places-autocomplete in the project.
AS temporary solution to my specific use case where I use the google map API's in two different components I have just added the script in the index.html:
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>
I did it in order to avoid that particular error as per of the documentation on the react-places-autocomplete GitHub page.
Unfortunately the link in the head of the index.html caused the same error. I found another workaround. Not the best solution, but works for now:
import React, { useEffect, useState } from 'react';
import GoogleMapReact from 'google-map-react';
export default () => {
const [mapActive, setMapActive] = useState(false);
useEffect(() => {
const t = setTimeout(() => {
setMapActive(true)
}, 100);
return () => {
window.clearTimeout(t);
};
}, [])
return (
<>
{ mapActive && <GoogleMapReact
bootstrapURLKeys={ {
key: ...,
language: ...
} }
defaultCenter={ ... }
defaultZoom={ ... }
>
</GoogleMapReact> }
</>
);
};
You could set a global variable and load the Google JavaScript only if the global variable is not set:
<script type="text/javascript">
if(document.isLoadingGoogleMapsApi===undefined) {
document.isLoadingGoogleMapsApi=true;
var script = document.createElement('script');
script.src='https://maps.googleapis.com/maps/api/js?key=[your-key]&callback=[yourInitMethodName]&v=weekly';
script.type='text/javascript';
script.defer=true;
document.getElementsByTagName('head')[0].appendChild(script);
}else{
[yourInitMethodName]();
}
</script>
In my case there is an arbitrary number of maps in a web application (starting at 0) and the user can add additional maps at runtime.
Most of the users do not use any map so loading it by default would cost unnecessarily loading time.

listview disappeared after setState({ds})

I am confused with the rerender mechanism of listview.
Page 1 have render a listview with two item, then I click 'Add' button, navigate to another page, and add one item to Page 1's datasource, then navigator back.
What I expect to see is Page 1 with three item, but actually is Page 2, listview is disappeared. But if I use mouse/finger touch it, listView come out again with three item.
I have test it on my iphone and simulator
Page 1's source code:
class Market extends Component {
constructor(props) {
super(props)
this.state = {
dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }),
}
}
componentDidMount() {
this.refreshListView(this.props.data)
}
componentWillReceiveProps(nextProps) {
this.refreshListView(nextProps.data)
}
refreshListView() {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(data)
})
}
render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this._renderRow}
refreshControl={
<RefreshControl/>
}
/>
)
}
const mapStateToProps = createSelector(
selectData(),
(data) => ({
data,
})
)
export default connect(mapStateToProps)(Market)
Just found something that has fixed mine! try adding removeClippedSubviews={false} to your ListView
https://github.com/facebook/react-native/issues/8607#issuecomment-231371923

React Bootstrap OverlayTrigger with trigger="focus" bug work around

In iOS safari, OverlayTrigger with trigger="focus" isn't able to dismiss when tapping outside. Here is my code:
<OverlayTrigger
trigger="focus"
placement="right"
overlay={ <Popover id="popoverID" title="Popover Title">
What a popover...
</Popover> } >
<a bsStyle="default" className="btn btn-default btn-circle" role="Button" tabIndex={18}>
<div className="btn-circle-text">?</div>
</a>
</OverlayTrigger>
I know that this is a known bug for Bootstrap cuz this doesn't even work on their own website in iOS, but does anyone know any method to go around it? It would be the best if it is something that doesn't require jQuery, but jQuery solution is welcome. Thanks.
OK, since no one else gives me a work around, I worked on this problem with my co-worker together for 3 days, and we came up with this heavy solution:
THE PROBLEM:
With trigger="focus", Bootstrap Popover/Tooltip can be dismissed when CLICKING outside the Popover/Tooltip, but not TOUCHING. Android browsers apparently changes touches to clicks automatically, so things are fine on Android. But iOS safari and browsers that is based on iOS safari (iOS chrome, iOS firefox, etc...) don't do that.
THE FIX:
We found out that in React Bootstrap, the Overlay component actually lets you customize when to show the Popover/Tooltip, so we built this component InfoOverlay based on Overlay. And to handle clicking outside the component, we need to add event listeners for both the Popover/Tooltip and window to handle both 'mousedown' and 'touchstart'. Also, this method would make the Popover have its smallest width all the time because of the padding-right of the component is initially 0px, and we make based on the width of some parent component so that it is responsive based on the parent component. And the code looks like this:
import React, { Component, PropTypes as PT } from 'react';
import {Popover, Overlay} from 'react-bootstrap';
export default class InfoOverlay extends Component {
static propTypes = {
PopoverId: PT.string,
PopoverTitle: PT.string,
PopoverContent: PT.node,
// You need to add this prop and pass it some numbers
// if you need to customize the arrowOffsetTop, it's sketchy...
arrowOffsetTop: PT.number,
// This is to be able to select the parent component
componentId: PT.string
}
constructor(props) {
super(props);
this.state = {
showPopover: false,
popoverClicked: false
};
}
componentDidMount() {
// Here are the event listeners and an algorithm
// so that clicking popover would not dismiss itself
const popover = document.getElementById('popoverTrigger');
if (popover) {
popover.addEventListener('mousedown', () => {
this.setState({
popoverClicked: true
});
});
popover.addEventListener('touchstart', () => {
this.setState({
popoverClicked: true
});
});
}
window.addEventListener('mousedown', () => {
if (!this.state.popoverClicked) {
this.setState({
showPopover: false
});
} else {
this.setState({
popoverClicked: false
});
}
});
window.addEventListener('touchstart', () => {
if (!this.state.popoverClicked) {
this.setState({
showPopover: false
});
} else {
this.setState({
popoverClicked: false
});
}
});
// this is to resize padding-right when window resizes
window.onresize = ()=>{
this.setState({});
};
}
// This function sets the style and more importantly, padding-right
getStyle() {
if (document.getElementById(this.props.componentId) && document.getElementById('popoverTrigger')) {
const offsetRight = document.getElementById(this.props.componentId).offsetWidth - document.getElementById('popoverTrigger').offsetLeft - 15;
return (
{display: 'inline-block', position: 'absolute', 'paddingRight': offsetRight + 'px'}
);
}
return (
{display: 'inline-block', position: 'absolute'}
);
}
overlayOnClick() {
this.setState({
showPopover: !(this.state.showPopover)
});
}
render() {
const customPopover = (props) => {
return (
{/* The reason why Popover is wrapped by another
invisible Popover is so that we can customize
the arrowOffsetTop, it's sketchy... */}
<div id="customPopover">
<Popover style={{'visibility': 'hidden', 'width': '100%'}}>
<Popover {...props} arrowOffsetTop={props.arrowOffsetTop + 30} id={this.props.PopoverId} title={this.props.PopoverTitle} style={{'marginLeft': '25px', 'marginTop': '-25px', 'visibility': 'visible'}}>
{this.props.PopoverContent}
</Popover>
</Popover>
</div>
);
};
return (
<div id="popoverTrigger" style={this.getStyle()}>
<a bsStyle="default" className="btn btn-default btn-circle" onClick={this.overlayOnClick.bind(this)} role="Button" tabIndex={13}>
<div id="info-button" className="btn-circle-text">?</div>
</a>
<Overlay
show={this.state.showPopover}
placement="right"
onHide={()=>{this.setState({showPopover: false});}}
container={this}>
{customPopover(this.props)}
</Overlay>
</div>
);
}
}
In the end, this is a heavy work around because it is a big amount of code for a fix, and you can probably feel your site is slowed down by a tiny bit because of the 4 event listeners. And the best solution is just tell Bootstrap to fix this problem...

How to set state to child component in React Native

I'm trying to create a React Native app for managing categories and items. It uses React Native Store to save all local data. The app should not and will not use an internet API. It will be local and offline only.
I managed to push items to the DB when calling the addCategory(title) function but it does not update the state inside categories.js
Since there is a spectacular lack of documentation concerning the state in react-native, I wondered if anyone here knows how to make it update through the listview and when changing from the categories to the items to details Components.
I'm using React Native 0.26
index.ios.js:
... // default imports
import Store from 'react-native-store'
const categorieslist = require('./categories')
const DB = {
'categories': Store.model('category'),
'items': Store.model('item')
}
class testapp extends Component {
componentDidMount() {
this.reload()
}
reload() {
DB.categories.find().then(resp => this.setState({
categories: resp
}))
}
addCategory(title) {
var newTitle = title;
DB.categories.add({
title: newTitle,
games: []
})
this.reload()
}
render() {
return (
<View>
<NavigatorIOS
...
initialRoute={{
component: categorieslist,
title: 'Categories',
rightButtonTitle: 'New',
onRightButtonPress: () => {this.addCategory('title')},
passProps: {
categories: this.state.categories
}
}}
/>
</View>
)
}
}
... // AppRegistry registerComponent and stuff
and categories.js:
... // default imports
class categories extends Component {
constructor(props) {
super(props)
var ds = new ListView.DataSource({rowHasChanged: (row1, row2) => row1 !== row2})
this.state = {
dataSource: ds.cloneWithRows(props.categories)
}
}
render() {
if (this.state.dataSource.getRowCount() === 0) {
... // shows 'no categories' screen
} else {
return(
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <View><Text>{rowData}</Text></View>}
/>
)
}
}
}
module.exports = categories
State in react-native is the same as react (web). So you can refer to the documentation here: https://facebook.github.io/react/docs/component-api.html
state is local to each component. In your reload method you setState to this which is the testapp Component and not the categories Component.
You must pass your state to the categories component via props.

Resources