-
Notifications
You must be signed in to change notification settings - Fork 526
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds ability to view and create notes from the customer details page (#…
…755) * Add notes basic wip adds ability to create note wip * Adds better note deletion handling * Revert unnecessary change * Addresses comments
- Loading branch information
Showing
7 changed files
with
252 additions
and
4 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
70 changes: 70 additions & 0 deletions
70
assets/src/components/customers/CustomerDetailsNewNoteInput.tsx
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import React, {useState} from 'react'; | ||
import * as API from '../../api'; | ||
import {Box} from 'theme-ui'; | ||
import logger from '../../logger'; | ||
import {CustomerNote} from '../../types'; | ||
import {TextArea, Text, Button} from '../common'; | ||
import {formatServerError} from '../../utils'; | ||
|
||
const CustomerDetailNewNoteInput = ({ | ||
customerId, | ||
onCreateNote, | ||
}: { | ||
customerId: string; | ||
onCreateNote: (note: CustomerNote) => void; | ||
}) => { | ||
const [isSaving, setIsSaving] = useState(false); | ||
const [errorMessage, setErrorMessage] = useState(''); | ||
const [note, setNote] = useState(''); | ||
|
||
const handleNoteChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||
setNote(e.target.value); | ||
}; | ||
|
||
const handleSaveNote = async () => { | ||
if (isSaving || note.length < 1) { | ||
return; | ||
} | ||
|
||
setIsSaving(true); | ||
|
||
try { | ||
const newNote = await API.createCustomerNote(customerId, note); | ||
onCreateNote(newNote); | ||
setNote(''); | ||
} catch (error) { | ||
logger.error('Error creating customer note:', error); | ||
const errorMessage = formatServerError(error); | ||
setErrorMessage(errorMessage); | ||
} | ||
|
||
setIsSaving(false); | ||
}; | ||
|
||
return ( | ||
<Box mb={2}> | ||
<TextArea | ||
style={{background: 'transparent'}} | ||
placeholder="Add a note" | ||
autoSize={{minRows: 2}} | ||
disabled={isSaving} | ||
value={note} | ||
onChange={handleNoteChange} | ||
/> | ||
{errorMessage && ( | ||
<Box mt={3}> | ||
<Text type="danger" strong> | ||
{errorMessage} | ||
</Text> | ||
</Box> | ||
)} | ||
<Box mt={3}> | ||
<Button onClick={handleSaveNote} disabled={isSaving || note.length < 1}> | ||
Add Note | ||
</Button> | ||
</Box> | ||
</Box> | ||
); | ||
}; | ||
|
||
export default CustomerDetailNewNoteInput; |
150 changes: 150 additions & 0 deletions
150
assets/src/components/customers/CustomerDetailsNotes.tsx
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import React from 'react'; | ||
import dayjs from 'dayjs'; | ||
import {Box, Flex} from 'theme-ui'; | ||
|
||
import {colors, Divider, Empty, Popconfirm, Text} from '../common'; | ||
import {CustomerNote} from '../../types'; | ||
import {formatRelativeTime} from '../../utils'; | ||
import * as API from '../../api'; | ||
import CustomerDetailsNewNoteInput from './CustomerDetailsNewNoteInput'; | ||
import logger from '../../logger'; | ||
import Spinner from '../Spinner'; | ||
|
||
type Props = {customerId: string}; | ||
type State = { | ||
customerNotes: CustomerNote[]; | ||
isLoading: boolean; | ||
}; | ||
|
||
class CustomerDetailsNotes extends React.Component<Props, State> { | ||
state: State = { | ||
customerNotes: [], | ||
isLoading: true, | ||
}; | ||
|
||
componentDidMount() { | ||
this.fetchCustomerNotes(); | ||
} | ||
|
||
fetchCustomerNotes = async () => { | ||
this.setState({isLoading: true}); | ||
|
||
try { | ||
const customerNotes = await API.fetchCustomerNotes(this.props.customerId); | ||
this.setState({customerNotes, isLoading: false}); | ||
} catch (error) { | ||
logger.error('Failed to fetch cutsomer notes', error); | ||
} | ||
}; | ||
|
||
handleCreateNote = () => { | ||
this.fetchCustomerNotes(); | ||
}; | ||
|
||
handleDeleteNote = async (note: CustomerNote) => { | ||
try { | ||
await API.deleteCustomerNote(note.id); | ||
await this.fetchCustomerNotes(); | ||
} catch (error) { | ||
logger.error('Failed to delete customer note', error); | ||
} | ||
}; | ||
|
||
render() { | ||
const {customerId} = this.props; | ||
const {isLoading, customerNotes} = this.state; | ||
|
||
if (isLoading) { | ||
return ( | ||
<Flex | ||
p={4} | ||
sx={{ | ||
flex: 1, | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
height: '100%', | ||
}} | ||
> | ||
<Spinner size={40} /> | ||
</Flex> | ||
); | ||
} | ||
|
||
return ( | ||
<Box p={3}> | ||
<CustomerDetailsNewNoteInput | ||
customerId={customerId} | ||
onCreateNote={this.handleCreateNote} | ||
/> | ||
|
||
<Divider dashed /> | ||
|
||
{customerNotes.length === 0 ? ( | ||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> | ||
) : ( | ||
<Box> | ||
{customerNotes | ||
.sort((a: CustomerNote, b: CustomerNote) => { | ||
return +new Date(b.created_at) - +new Date(a.created_at); | ||
}) | ||
.map((note) => ( | ||
<CustomerDetailNote | ||
key={note.id} | ||
note={note} | ||
onDeleteNote={this.handleDeleteNote} | ||
/> | ||
))} | ||
</Box> | ||
)} | ||
</Box> | ||
); | ||
} | ||
} | ||
|
||
const CustomerDetailNote = ({ | ||
note, | ||
onDeleteNote, | ||
}: { | ||
note: CustomerNote; | ||
onDeleteNote: (note: CustomerNote) => void; | ||
}) => { | ||
const {created_at: createdAt} = note; | ||
let authorIdentifier; | ||
|
||
if (note.author) { | ||
const {display_name: displayName, full_name: fullName, email} = note.author; | ||
const authorName = displayName || fullName; | ||
authorIdentifier = !!authorName ? `${authorName} · ${email}` : email; | ||
} | ||
|
||
return ( | ||
<Popconfirm | ||
title="Delete this note?" | ||
okText="Delete" | ||
cancelText="Cancel" | ||
placement="left" | ||
onConfirm={() => onDeleteNote(note)} | ||
> | ||
<Box | ||
py={2} | ||
px={3} | ||
mb={2} | ||
sx={{ | ||
bg: colors.note, | ||
borderRadius: 2, | ||
cursor: 'pointer', | ||
}} | ||
> | ||
<Box mb={3} sx={{whiteSpace: 'break-spaces'}}> | ||
{note.body} | ||
</Box> | ||
<Flex sx={{justifyContent: 'space-between'}}> | ||
<Text type="secondary">{authorIdentifier}</Text> | ||
<Text type="secondary">{formatRelativeTime(dayjs(createdAt))}</Text> | ||
</Flex> | ||
</Box> | ||
</Popconfirm> | ||
); | ||
}; | ||
|
||
export default CustomerDetailsNotes; |
This file contains 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
This file contains 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
This file contains 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