diff --git a/assets/src/api.ts b/assets/src/api.ts index 2cf87a67c..76afbc18d 100644 --- a/assets/src/api.ts +++ b/assets/src/api.ts @@ -175,6 +175,24 @@ export const fetchCustomer = async (id: string, token = getAccessToken()) => { .then((res) => res.body.data); }; +export const updateCustomer = async ( + id : string, + updates: any, + token = getAccessToken() +) => { + if (!token) { + throw new Error('Invalid token!'); + } + + return request + .put(`/api/customers/${id}`) + .set('Authorization', token) + .send({ + customer: updates, + }) + .then((res) => res.body.data); +}; + export const createNewConversation = async ( accountId: string, customerId: string diff --git a/assets/src/components/customers/CustomerDetailsModal.tsx b/assets/src/components/customers/CustomerDetailsModal.tsx index fe503db78..a31580f10 100644 --- a/assets/src/components/customers/CustomerDetailsModal.tsx +++ b/assets/src/components/customers/CustomerDetailsModal.tsx @@ -3,7 +3,10 @@ import {Box, Flex} from 'theme-ui'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import {capitalize} from 'lodash'; -import {Button, Modal, Paragraph, Text} from '../common'; +import {Button, Modal, Paragraph, Text, Input} from '../common'; +import * as API from '../../api'; +import {Customer} from '../../types'; +import logger from '../../logger'; // TODO: create date utility methods so we don't have to do this everywhere dayjs.extend(utc); @@ -11,7 +14,7 @@ dayjs.extend(utc); export const CustomerMetadataSection = ({ metadata, }: { - metadata: Record; + metadata?: Record; }) => { return !metadata ? null : ( @@ -43,134 +46,270 @@ export const CustomerMetadataSection = ({ ); }; -export const CustomerDetailsContent = ({customer}: {customer: any}) => { - const { - email, - name, - browser, - os, - phone, - external_id: externalId, - created_at: createdAt, - updated_at: lastUpdatedAt, - current_url: lastSeenUrl, - ip: lastIpAddress, - metadata, - time_zone, - } = customer; - return ( - - - - Name - +type Props = { + customer: Customer; + isVisible?: boolean; + onClose: () => void; + onUpdate: (data: any) => Promise; +}; - {name || 'Unknown'} - +type State = { + updates: any; + isEditing: boolean; + isSaving: boolean; +}; - - - - Email - +class CustomerDetailsModal extends React.Component { + state: State = { + updates: this.getInitialUpdates(), + isEditing: false, + isSaving: false, + }; - {email || 'Unknown'} - - - - Phone - + getInitialUpdates() { + const {customer} = this.props; + const editableFieldsWhitelist: Array = [ + 'name', + 'email', + 'phone', + ]; - {phone || 'Unknown'} - - - - - - ID - + return editableFieldsWhitelist.reduce((acc, field) => { + return {...acc, [field]: customer[field] || null}; + }, {}); + } - {externalId || 'Unknown'} - - - - Time zone - + handleStartEditing = () => { + this.setState({isEditing: true}); + }; - {time_zone || 'Unknown'} - - - - - Device information - + handleCancelEdit = () => { + this.setState({ + updates: this.getInitialUpdates(), + isEditing: false, + }); + }; - - {[lastIpAddress, os, browser].join(' · ') || 'N/A'} - - + handleEditCustomer = (updates: any) => { + this.setState({updates: {...this.state.updates, ...updates}}); + }; + + handleChangeName = (e: any) => { + this.handleEditCustomer({name: e.target.value}); + }; + + handleChangeEmail = (e: any) => { + this.handleEditCustomer({email: e.target.value}); + }; + + handleChangePhone = (e: any) => { + this.handleEditCustomer({phone: e.target.value}); + }; + + handleSaveUpdates = async () => { + this.setState({isSaving: true}); + + const {customer, onUpdate} = this.props; + const {updates} = this.state; + const {id: customerId} = customer; + + try { + const result = await API.updateCustomer(customerId, updates); + + await onUpdate(result); + } catch (err) { + logger.error('Failed to update customer', err); + } - + this.handleCancelEdit(); + this.setState({isSaving: false}); + }; + + onModalClose = () => { + this.handleCancelEdit(); + this.props.onClose(); + }; + + render() { + const {customer, isVisible} = this.props; + const {isEditing, isSaving, updates} = this.state; + const { + browser, + os, + email, + name, + phone, + external_id: externalId, + created_at: createdAt, + updated_at: lastUpdatedAt, + current_url: lastSeenUrl, + ip: lastIpAddress, + metadata, + time_zone, + } = customer; + + return ( + + + + + + + + ) : ( + + + + + ) + } + > - Last visited URL - + + + Name + + {!isEditing ? ( + {name || 'Unknown'} + ) : ( + + + + )} + - {lastSeenUrl ? ( - - {lastSeenUrl} - - ) : ( - N/A - )} - + + + + Email + + {!isEditing ? ( + {email || 'Unknown'} + ) : ( + + + + )} + + + + Phone + + {!isEditing ? ( + {phone || 'Unknown'} + ) : ( + + + + )} + + + + + + + ID + - - - - First seen + {externalId || 'Unknown'} + + + + Time zone + + + {time_zone || 'Unknown'} + + + + + Device information + + + + {[lastIpAddress, os, browser].join(' · ') || 'N/A'} + - - {createdAt ? dayjs.utc(createdAt).format('MMMM DD, YYYY') : 'N/A'} - - + + + Last visited URL + - - - Last seen + {lastSeenUrl ? ( + + {lastSeenUrl} + + ) : ( + N/A + )} - - {lastUpdatedAt - ? dayjs.utc(lastUpdatedAt).format('MMMM DD, YYYY') - : 'N/A'} - - - + + + + First seen + - - - ); -}; + + {createdAt + ? dayjs.utc(createdAt).format('MMMM DD, YYYY') + : 'N/A'} + + -type Props = { - customer: any; - isVisible?: boolean; - onClose: () => void; -}; + + + Last seen + -const CustomerDetailsModal = ({customer, isVisible, onClose}: Props) => { - return ( - Close} - > - - - ); -}; + + {lastUpdatedAt + ? dayjs.utc(lastUpdatedAt).format('MMMM DD, YYYY') + : 'N/A'} + + + + + + + + ); + } +} export default CustomerDetailsModal; diff --git a/assets/src/components/customers/CustomersPage.tsx b/assets/src/components/customers/CustomersPage.tsx index 7f2b0d20f..053873c1f 100644 --- a/assets/src/components/customers/CustomersPage.tsx +++ b/assets/src/components/customers/CustomersPage.tsx @@ -12,6 +12,7 @@ type Props = { }; type State = { loading: boolean; + refreshing: boolean; selectedCustomerId: string | null; customers: Array; }; @@ -19,6 +20,7 @@ type State = { class CustomersPage extends React.Component { state: State = { loading: true, + refreshing: false, selectedCustomerId: null, customers: [], }; @@ -35,9 +37,23 @@ class CustomersPage extends React.Component { } } + handleRefreshCustomers = async () => { + this.setState({refreshing: true}); + + try { + const customers = await API.fetchCustomers(); + + this.setState({customers, refreshing: false}); + } catch (err) { + logger.error('Error refreshing customers!', err); + + this.setState({refreshing: false}); + } + }; + render() { const {currentlyOnline} = this.props; - const {loading, customers = []} = this.state; + const {loading, refreshing, customers = []} = this.state; if (loading) { return ( @@ -78,8 +94,10 @@ class CustomersPage extends React.Component { diff --git a/assets/src/components/customers/CustomersTable.tsx b/assets/src/components/customers/CustomersTable.tsx index 645aa0043..ebd27c14c 100644 --- a/assets/src/components/customers/CustomersTable.tsx +++ b/assets/src/components/customers/CustomersTable.tsx @@ -9,11 +9,15 @@ import CustomerDetailsModal from './CustomerDetailsModal'; dayjs.extend(utc); const CustomersTable = ({ + loading, customers, currentlyOnline, + onUpdate, }: { + loading?: boolean; customers: Array; currentlyOnline: any; + onUpdate: () => Promise; }) => { const [selectedCustomerId, setSelectedCustomerId] = React.useState(null); @@ -119,6 +123,7 @@ const CustomersTable = ({ customer={record} isVisible={selectedCustomerId === record.id} onClose={() => setSelectedCustomerId(null)} + onUpdate={onUpdate} /> ); @@ -126,7 +131,7 @@ const CustomersTable = ({ }, ]; - return ; + return
; }; export default CustomersTable; diff --git a/assets/src/logger.tsx b/assets/src/logger.tsx index 6844f6de4..1197e8e52 100644 --- a/assets/src/logger.tsx +++ b/assets/src/logger.tsx @@ -64,6 +64,7 @@ export class Logger { } error(...args: any) { + // TODO: capture these errors in Sentry? console.error(...args); this.callback('error', ...args); } diff --git a/assets/src/types.ts b/assets/src/types.ts index b0dbe30dc..cdb41ff57 100644 --- a/assets/src/types.ts +++ b/assets/src/types.ts @@ -21,7 +21,7 @@ export type Customer = { host?: string; ip?: string; last_seen?: string; - metadata?: object; + metadata?: any; os?: string; pathname?: string; phone?: number;