Skip to content

Instantly share code, notes, and snippets.

@jacobmoyle
Created May 30, 2019 00:01
Show Gist options
  • Select an option

  • Save jacobmoyle/ee288f95ab39e7c9df968d981789c875 to your computer and use it in GitHub Desktop.

Select an option

Save jacobmoyle/ee288f95ab39e7c9df968d981789c875 to your computer and use it in GitHub Desktop.
// Share Modal Share Modal Share Modal Share Modal Share Modal Share Modal Share Modal Share Modal Share Modal
// Share Modal
// Share Modal Share Modal Share Modal Share Modal Share Modal Share Modal Share Modal Share Modal Share Modal
// Share Modal Copy
{
"title": "Share Live Updates",
"body": "Share with people inside or outside your organization:"
}
// Share Modal
import React from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import isEmpty from 'lodash/isEmpty'
import { fetchActiveToken, fetchToken, getLoading, getToken, sendEmail } from 'Store/share'
import { fullNameSelector, emailFromState } from 'Store/auth/user'
import { showNotification } from 'Store/notifications'
import { ROLLUPS } from 'Utils/shipmentUtils'
import buildUrl from 'Utils/urlBuilder'
import CloseIcon from '@material-ui/icons/Close'
import IconButton from '@material-ui/core/IconButton'
import Typography from '@material-ui/core/Typography'
import Dialog from '@material-ui/core/Dialog'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import ShareForm from './ShareForm'
import Loader from 'AppComponents/Loader'
import c from './content'
import { withStyles } from '@material-ui/core/styles'
import styles from './styles'
class ShareModal extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
fetchActiveToken: PropTypes.func.isRequired,
fetchToken: PropTypes.func.isRequired,
isLoading: PropTypes.bool,
onClose: PropTypes.func,
open: PropTypes.bool,
sendEmail: PropTypes.func.isRequired,
shipments: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired
})
),
showNotification: PropTypes.func.isRequired,
token: PropTypes.string,
refType: PropTypes.oneOf(ROLLUPS),
userFullName: PropTypes.string,
userEmail: PropTypes.string
}
constructor(props) {
super(props)
this.inputRef = React.createRef()
}
handleClose = () => {
this.props.onClose()
}
focusAndSelectLink = () => {
if (this.props.token && this.inputRef.current) {
this.inputRef.current.focus()
this.inputRef.current.select()
}
}
componentDidMount() {
const { fetchActiveToken, shipments, refType } = this.props
fetchActiveToken({ shipments, refType })
}
handleFormSubmit = formData => {
const { shipments, refType, sendEmail } = this.props
sendEmail({ shipments, refType, formData })
}
handleCreateToken = () => {
const { shipments, refType, fetchToken } = this.props
fetchToken({ shipments, refType })
// TODO: The next line is dumb and hacky. Let's fix it ASAP.
setTimeout(() => {
this.focusAndSelectLink()
}, 500)
}
render() {
const {
classes,
isLoading,
open,
showNotification,
token,
userFullName,
userEmail
} = this.props
return (
<Dialog
aria-labelledby="share-dialog-title"
aria-describedby="share-modal-description"
open={open}
classes={{ paper: classes.dialogModal }}
onEntered={this.focusAndSelectLink}
onClose={this.handleClose}
fullWidth
>
<DialogTitle id="share-dialog-title" className={classes.dialogTitle} disableTypography>
<Typography className={classes.header} variant="h6">
{c.title}
</Typography>
<IconButton onClick={this.handleClose}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<DialogContentText className={classes.padBttm}>{c.body}</DialogContentText>
{isLoading ? (
<div className={classes.loaderContainer}>
<Loader />
</div>
) : (
<React.Fragment>
<ShareForm
currentUser={{
name: userFullName,
email: userEmail
}}
onTokenGenerate={this.handleCreateToken}
token={isEmpty(token) ? '' : buildUrl({ path: `/s/${token}` })}
onCopySuccess={e => showNotification('Copied link to clipboard.')}
onSubmit={this.handleFormSubmit}
/>
</React.Fragment>
)}
</DialogContent>
</Dialog>
)
}
}
const mapDispatchToProps = {
fetchActiveToken,
fetchToken,
sendEmail,
showNotification
}
const mapStateToProps = (state, ownProps) => ({
isLoading: getLoading(state),
token: getToken(state, ownProps.shipments),
userFullName: fullNameSelector(state),
userEmail: emailFromState(state)
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ShareModal))
// Share Form Share Form Share Form Share Form Share Form Share Form Share Form Share Form Share Form
// Share Form
// Share Form Share Form Share Form Share Form Share Form Share Form Share Form Share Form Share Form
// Share Form Copy
{
"submitBttn": "Send",
"emailPlaceholder": "Enter email addresses",
"subjectLabel": "Subject Line",
"messageLabel": "Optional message",
"tokenText": "Anyone with access to this link can view basic shipment information. Automatically expires in 90 days unless revoked.",
"validationErrorSubject": "Required",
"validationErrorEmails": "Requires valid email",
"subjectPlaceholder": "shared shipments with you"
}
// Share Form Utils
import c from './content'
import { ENTER, COMMA, SPACE } from 'Utils/syntheticEvents'
export const SYNTHETIC_EVENTS = [ENTER, COMMA, SPACE]
export const getValidEmails = chipConfigs =>
chipConfigs
.filter(configs => !configs.hasError)
.map(validConfigs => validConfigs.label)
.filter((val, index, self) => self.indexOf(val) === index)
export const getSubjectLine = user => {
if (user.name) {
return `${user.name} ${c.subjectPlaceholder}`
}
if (user.email) {
return `${user.email} ${c.subjectPlaceholder}`
}
return ''
}
// Share Form
import React from 'react'
import PropTypes from 'prop-types'
import validate from 'validate.js'
import { getValidEmails, SYNTHETIC_EVENTS, getSubjectLine } from './utils'
import Typography from '@material-ui/core/Typography'
import Button from '@material-ui/core/Button'
import FormControl from '@material-ui/core/FormControl'
import TextField from 'AppComponents/TextField'
import GenerateToken from './GenerateToken'
import ChipInput from 'AppComponents/clearmetal/core/ChipInput'
import c from './content'
import styles from './styles'
import { withStyles } from '@material-ui/core/styles'
class ShareForm extends React.Component {
constructor(props) {
super(props)
const { currentUser } = props
this.state = {
errors: {
emailField: '',
subjectField: ''
},
body: '',
subject: getSubjectLine(currentUser),
chipConfigs: [],
selectedChipId: null,
emailInput: ''
}
}
static propTypes = {
classes: PropTypes.object.isRequired,
currentUser: PropTypes.shape({
name: PropTypes.string,
email: PropTypes.string.isRequired
}),
onSubmit: PropTypes.func.isRequired,
token: PropTypes.string,
onTokenGenerate: PropTypes.func.isRequired,
onCopySuccess: PropTypes.func.isRequired
}
handleAddChipConfig = e => {
e.preventDefault()
const newEmail = e.target.value
const emailFormatValid = validate({ from: newEmail }, { from: { email: true } }) === undefined
// Append new email as a chip
this.setState(prevState => {
return {
emailInput: '',
chipConfigs: prevState.chipConfigs.concat({
label: newEmail,
valid: emailFormatValid
})
}
})
}
handleRemoveChipConfig = (idx, e) => {
// Remove chip by index
this.setState(prevState => {
const { chipConfigs } = prevState
let modifiedConfigCopy = [...chipConfigs]
modifiedConfigCopy.splice(idx, 1)
return {
chipConfigs: modifiedConfigCopy
}
})
}
handleEmailInputChange = e => {
const newInput = e.target.value
// Only store non-delimeter input
if (SYNTHETIC_EVENTS.includes(newInput)) return
this.setState({ emailInput: newInput })
}
validateForm = () => {
const { subject, chipConfigs } = this.state
let errors = {
emailField: '',
subjectField: ''
}
if (subject === '') errors.subjectField = c.validationErrorSubject
if (getValidEmails(chipConfigs).length === 0) errors.emailField = c.validationErrorEmails
this.setState({
errors
})
}
hasValidationErrors = () => {
const { errors } = this.state
return Object.values(errors).every(fieldError => {
return fieldError === ''
})
}
handleSubmit = e => {
e.preventDefault()
e.stopPropagation()
const { subject, body, chipConfigs } = this.state
const { onSubmit } = this.props
// Run validations
this.validateForm()
// Exit if form contains errors
if (this.hasValidationErrors()) return
// Submit if no errors
onSubmit({
body,
subject,
emails: chipConfigs.map(config => config.label),
validEmails: getValidEmails(chipConfigs)
})
}
handleEditChip = (id, targetConfig) => {
this.setState({
emailInput: targetConfig.label
})
this.handleRemoveChipConfig(id)
}
handleChipSelection = id => {
const { chipConfigs, selectedChipId } = this.state
const targetConfig = chipConfigs[id]
const isSecondClick = selectedChipId === id
const isSelectionInvalid = !targetConfig.valid
if (isSelectionInvalid) {
// One click to-edit-if-invalid
this.handleEditChip(id, targetConfig)
} else if (isSecondClick) {
// Edit on second click if valid
this.handleEditChip(id, targetConfig)
} else {
this.setState({ selectedChipId: id })
}
}
render() {
const { subject, body, chipConfigs, emailInput, errors } = this.state
const { classes, onTokenGenerate, token, onCopySuccess } = this.props
return (
<form className={classes.container} onSubmit={e => e.preventDefault()}>
<FormControl className={classes.padBttm}>
<ChipInput
onChipSelect={id => this.handleChipSelection(id)}
value={emailInput}
onChange={this.handleEmailInputChange}
chipConfigs={chipConfigs}
newChipSyntheticKeyCodes={SYNTHETIC_EVENTS}
onAddRequest={this.handleAddChipConfig}
onDeleteRequest={this.handleRemoveChipConfig}
placeholder={chipConfigs.length === 0 ? c.emailPlaceholder : null}
helperText={errors.emailField}
/>
</FormControl>
<FormControl className={classes.padBttm}>
<TextField
label={c.subjectLabel}
value={subject}
helperText={errors.subjectField}
onChange={e => this.setState({ subject: e.target.value })}
onClearInput={e => this.setState({ subject: '' })}
/>
</FormControl>
<FormControl className={classes.padBttmLarge}>
<TextField
value={body}
onChange={e => this.setState({ body: e.currentTarget.value })}
label={c.messageLabel}
onClearInput={e => this.setState({ body: '' })}
multiline
rows="3"
/>
</FormControl>
<FormControl>
{token && (
<Typography className={classes.tokenText} color="primary">
{c.tokenText}
</Typography>
)}
<div className={classes.buttonWrapper}>
<GenerateToken
className={classes.generateTokenContainer}
inputContainerClassName={classes.tokenField}
generateToken={onTokenGenerate}
inputRef={this.inputRef}
onCopiedToClipboard={onCopySuccess}
token={token}
/>
<Button
classes={{
label: classes.label
}}
className={classes.submitBttn}
color="primary"
onClick={this.handleSubmit}
type="submit"
variant="contained"
size="large"
>
{c.submitBttn}
</Button>
</div>
</FormControl>
</form>
)
}
}
export default withStyles(styles)(ShareForm)
// Chip Input Chip Input Chip Input Chip Input Chip Input Chip Input Chip Input Chip Input Chip Input
// Chip Input
// Chip Input Chip Input Chip Input Chip Input Chip Input Chip Input Chip Input Chip Input Chip Input
import React from 'react'
import PropTypes from 'prop-types'
import { ENTER, DELETE, EMPTY } from 'Utils/syntheticEvents'
import Chip from 'AppComponents/clearmetal/core/Chip'
import TextField from '@material-ui/core/TextField'
import classnames from 'classnames'
import { withStyles } from '@material-ui/core/styles'
import styles from './styles'
// TODO: Handle paste with regex and splitting?
class ChipInput extends React.Component {
state = {
hovered: false
}
static defaultProps = {
required: false,
newChipSyntheticKeyCodes: [ENTER]
}
static propTypes = {
newChipOnBlur: PropTypes.bool,
required: PropTypes.bool.isRequired,
onAddRequest: PropTypes.func.isRequired,
onDeleteRequest: PropTypes.func.isRequired,
chipConfigs: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
valid: PropTypes.bool.isRequired
})
),
newChipSyntheticKeyCodes: PropTypes.arrayOf(PropTypes.string),
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
onChipSelect: PropTypes.func
}
handleChipUpdate = e => {
const { newChipSyntheticKeyCodes, onAddRequest, onDeleteRequest, chipConfigs } = this.props
const value = e.target.value
// Delete Chip
if (e.key === DELETE && value === EMPTY) {
onDeleteRequest(e, chipConfigs.length - 1)
}
// Add Chip
if (newChipSyntheticKeyCodes.includes(e.key) && value !== EMPTY) {
onAddRequest(e)
}
}
render() {
const { hovered } = this.state
const {
chipConfigs,
children,
classes,
onMouseEnter,
onMouseLeave,
onDeleteRequest,
required,
newChipOnBlur,
onChipSelect,
onAddRequest,
newChipSyntheticKeyCodes,
...rest
} = this.props
const chips = chipConfigs.map((config, index) => {
console.log(index)
return (
<div key={index} className={classes.chipPadding}>
<Chip
key={index}
onClick={e => onChipSelect(index, e)}
onDelete={e => onDeleteRequest(index, e)}
clickable={Boolean(onChipSelect)}
hasError={!config.valid}
{...config}
/>
</div>
)
})
return (
<TextField
variant="filled"
required={required && chipConfigs.length === 0}
onKeyDown={this.handleChipUpdate}
onMouseEnter={e => {
this.setState({ hovered: true })
onMouseEnter && onMouseEnter(e) // call the user's function if they pass one in
}}
onMouseLeave={e => {
this.setState({ hovered: false })
onMouseLeave && onMouseLeave(e) // call the user's function if they pass one in
}}
InputProps={{
classes: {
root: classes.inputRoot,
input: classes.input,
underline: classnames(classes.inputUnderline, {
[classes.nonHoveredInputUnderline]: !hovered
}),
disabled: classes.disabledInputUnderline,
error: classes.inputError
},
startAdornment: chips
}}
{...rest}
/>
)
}
}
export default withStyles(styles)(ChipInput)
// Token Generate Token Generate Token Generate Token Generate Token Generate Token Generate Token Generate
// Token Generate
// Token Generate Token Generate Token Generate Token Generate Token Generate Token Generate Token Generate
// Token Generate Copy
{
"button": "Get Shareable Link",
"tooltip": "Copy",
"tooltipAria": "Copy to Clipboard"
}
// Token Generate
import React from 'react'
import PropTypes from 'prop-types'
import copy from 'copy-to-clipboard'
import isEmpty from 'lodash/isEmpty'
import Button from '@material-ui/core/Button'
import OutlinedInput from '@material-ui/core/OutlinedInput'
import Tooltip from '@material-ui/core/Tooltip'
import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment'
import FormControl from '@material-ui/core/FormControl'
import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'
import LinkIcon from '@material-ui/icons/Link'
import c from './content'
import styles from './styles'
import { withStyles } from '@material-ui/core/styles'
import cx from 'classnames'
const GenerateToken = ({
token,
generateToken,
classes,
className,
inputClassName,
inputContainerClassName,
buttonContainerClassName,
onCopiedToClipboard,
inputRef
}) => {
const showButton = isEmpty(token)
const button = (
<Button
color="primary"
variant="outlined"
size="large"
className={classes.flatButton}
onClick={e => {
e.preventDefault()
generateToken()
}}
classes={{
label: classes.label
}}
>
<LinkIcon className={classes.leftIcon} />
{c.button}
</Button>
)
const inputField = (
<OutlinedInput
id="generate-token"
className={inputClassName}
classes={{
root: classes.inputRoot
}}
inputRef={inputRef}
value={token}
onClick={e => {
if (e.target && e.target.select) {
e.target.select()
}
}}
margin="dense"
readOnly
startAdornment={
<InputAdornment
classes={{
root: classes.positionStart
}}
position="start"
>
<Tooltip title={c.tooltip} placement="top">
<IconButton
onClick={() => {
copy(token)
onCopiedToClipboard()
}}
className={classes.iconButton}
aria-label={c.tooltipAria}
>
<FileCopyOutlinedIcon className={classes.copyIcon} />
</IconButton>
</Tooltip>
</InputAdornment>
}
/>
)
return (
<FormControl
className={cx(
classes.container,
className,
showButton ? buttonContainerClassName : inputContainerClassName
)}
>
{showButton ? button : inputField}
</FormControl>
)
}
GenerateToken.propTypes = {
token: PropTypes.string.isRequired,
generateToken: PropTypes.func.isRequired,
classes: PropTypes.object.isRequired,
className: PropTypes.string.isRequired, // Applied to top level DOM element
inputClassName: PropTypes.string.isRequired, // Used to Style Input Field directly
inputContainerClassName: PropTypes.string.isRequired, // Used to Style Input Field Container directly
buttonContainerClassName: PropTypes.string.isRequired, // Used to Style Button Container directly
onCopiedToClipboard: PropTypes.func.isRequired
}
GenerateToken.defaultProps = {
token: null,
className: '',
inputClassName: '',
inputContainerClassName: '',
buttonContainerClassName: ''
}
export default withStyles(styles)(GenerateToken)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment