Instantly share code, notes, and snippets.
Created
May 30, 2019 00:01
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save jacobmoyle/ee288f95ab39e7c9df968d981789c875 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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