-
Notifications
You must be signed in to change notification settings - Fork 559
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Stop passing unneeded buckets as props * Simplify mapDispatchToProps * Add prop type checks * Rename NavigationBar CSS classes to BEM * Add basic SyncStatus component * Recount unsynced changes * Set connection status * Add popover * Add design for unsynced note list * Get/set last synced time * Update material-ui to avoid deprecation warning * Get unsynced notes * Tweak popover styles * Fix keyboard accessibility * Set limit on number of notes in unsynced list
- Loading branch information
Showing
18 changed files
with
563 additions
and
119 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { compact } from 'lodash'; | ||
import noteTitleAndPreview from '../../utils/note-utils'; | ||
|
||
const getNoteTitles = (ids, notes, limit = Infinity) => { | ||
const matchedNotes = ids.map((id, i) => { | ||
if (i >= limit) { | ||
return; | ||
} | ||
|
||
const note = notes.find(thisNote => thisNote.id === id); | ||
|
||
if (!note) { | ||
// eslint-disable-next-line no-console | ||
console.log(`Could not find note with id '${id}'`); | ||
return null; | ||
} | ||
|
||
return { id, title: noteTitleAndPreview(note).title }; | ||
}); | ||
|
||
return compact(matchedNotes); | ||
}; | ||
|
||
export default getNoteTitles; |
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,46 @@ | ||
import getNoteTitles from './get-note-titles'; | ||
|
||
describe('getNoteTitles', () => { | ||
const originalConsoleLog = console.log; // eslint-disable-line no-console | ||
|
||
afterEach(() => { | ||
global.console.log = originalConsoleLog; | ||
}); | ||
|
||
it('should return the titles for the given note ids', () => { | ||
const result = getNoteTitles( | ||
['foo', 'baz'], | ||
[ | ||
{ id: 'foo', data: { content: 'title\nexcerpt', systemTags: [] } }, | ||
{ id: 'bar' }, | ||
{ id: 'baz', data: { content: 'title\nexcerpt', systemTags: [] } }, | ||
] | ||
); | ||
expect(result).toEqual([ | ||
{ id: 'foo', title: 'title' }, | ||
{ id: 'baz', title: 'title' }, | ||
]); | ||
}); | ||
|
||
it('should not choke on invalid ids', () => { | ||
global.console.log = jest.fn(); | ||
const result = getNoteTitles( | ||
['foo', 'bar'], | ||
[{ id: 'foo', data: { content: 'title', systemTags: [] } }] | ||
); | ||
expect(result).toEqual([{ id: 'foo', title: 'title' }]); | ||
}); | ||
|
||
it('should return no more than `limit` items', () => { | ||
const limit = 1; | ||
const result = getNoteTitles( | ||
['foo', 'bar'], | ||
[ | ||
{ id: 'foo', data: { content: 'title', systemTags: [] } }, | ||
{ id: 'bar' }, | ||
], | ||
limit | ||
); | ||
expect(result).toHaveLength(limit); | ||
}); | ||
}); |
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,66 @@ | ||
import React, { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import AlertIcon from '../../icons/alert'; | ||
import SyncIcon from '../../icons/sync'; | ||
import SyncStatusPopover from './popover'; | ||
|
||
class SyncStatus extends Component { | ||
static propTypes = { | ||
isOffline: PropTypes.bool.isRequired, | ||
unsyncedNoteIds: PropTypes.array.isRequired, | ||
}; | ||
|
||
state = { | ||
anchorEl: null, | ||
}; | ||
|
||
handlePopoverOpen = event => { | ||
this.setState({ anchorEl: event.currentTarget }); | ||
}; | ||
|
||
handlePopoverClose = () => { | ||
this.setState({ anchorEl: null }); | ||
}; | ||
|
||
render() { | ||
const { isOffline, unsyncedNoteIds } = this.props; | ||
const { anchorEl } = this.state; | ||
|
||
const popoverId = 'sync-status__popover'; | ||
|
||
const unsyncedChangeCount = unsyncedNoteIds.length; | ||
const unit = unsyncedChangeCount === 1 ? 'change' : 'changes'; | ||
const text = unsyncedChangeCount | ||
? `${unsyncedChangeCount} unsynced ${unit}` | ||
: isOffline ? 'No connection' : 'All changes synced'; | ||
|
||
return ( | ||
<div> | ||
<div | ||
className="sync-status" | ||
aria-owns={anchorEl ? popoverId : undefined} | ||
aria-haspopup="true" | ||
onFocus={this.handlePopoverOpen} | ||
onMouseEnter={this.handlePopoverOpen} | ||
onMouseLeave={this.handlePopoverClose} | ||
tabIndex="0" | ||
> | ||
<span className="sync-status__icon"> | ||
{isOffline ? <AlertIcon /> : <SyncIcon />} | ||
</span> | ||
{text} | ||
</div> | ||
|
||
<SyncStatusPopover | ||
anchorEl={anchorEl} | ||
id={popoverId} | ||
onClose={this.handlePopoverClose} | ||
unsyncedNoteIds={unsyncedNoteIds} | ||
/> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default SyncStatus; |
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,110 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { connect } from 'react-redux'; | ||
import classnames from 'classnames'; | ||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; | ||
import Popover from '@material-ui/core/Popover'; | ||
|
||
import { getLastSyncedTime } from '../../utils/sync/last-synced-time'; | ||
import getNoteTitles from './get-note-titles'; | ||
|
||
class SyncStatusPopover extends React.Component { | ||
render() { | ||
const { | ||
anchorEl, | ||
classes = {}, | ||
id, | ||
notes, | ||
onClose, | ||
theme, | ||
unsyncedNoteIds, | ||
} = this.props; | ||
const themeClass = `theme-${theme}`; | ||
const open = Boolean(anchorEl); | ||
const hasUnsyncedChanges = unsyncedNoteIds.length > 0; | ||
|
||
const QUERY_LIMIT = 10; | ||
const noteTitles = hasUnsyncedChanges | ||
? getNoteTitles(unsyncedNoteIds, notes, QUERY_LIMIT) | ||
: []; | ||
const overflowCount = unsyncedNoteIds.length - noteTitles.length; | ||
const unit = overflowCount === 1 ? 'note' : 'notes'; | ||
|
||
const lastSyncedTime = distanceInWordsToNow(getLastSyncedTime(), { | ||
addSuffix: true, | ||
}); | ||
|
||
return ( | ||
<Popover | ||
id={id} | ||
className={classnames( | ||
'sync-status-popover', | ||
classes.popover, | ||
themeClass | ||
)} | ||
classes={{ | ||
paper: classnames( | ||
'sync-status-popover__paper', | ||
'theme-color-bg', | ||
'theme-color-border', | ||
'theme-color-fg-dim', | ||
{ 'has-unsynced-changes': hasUnsyncedChanges }, | ||
classes.paper | ||
), | ||
}} | ||
open={open} | ||
anchorEl={anchorEl} | ||
anchorOrigin={{ | ||
vertical: 'center', | ||
horizontal: 'left', | ||
}} | ||
transformOrigin={{ | ||
vertical: 'bottom', | ||
horizontal: 'center', | ||
}} | ||
onBlur={onClose} | ||
onClose={onClose} | ||
PaperProps={{ square: true }} | ||
disableRestoreFocus | ||
> | ||
{hasUnsyncedChanges && ( | ||
<div className="sync-status-popover__unsynced theme-color-border"> | ||
<h2 className="sync-status-popover__heading"> | ||
Notes with unsynced changes | ||
</h2> | ||
<ul className="sync-status-popover__notes theme-color-fg"> | ||
{noteTitles.map(note => <li key={note.id}>{note.title}</li>)} | ||
</ul> | ||
{!!overflowCount && ( | ||
<p> | ||
and {overflowCount} more {unit} | ||
</p> | ||
)} | ||
<div> | ||
If a note isn’t syncing, try switching networks or editing the | ||
note again. | ||
</div> | ||
</div> | ||
)} | ||
<span>Last synced: {lastSyncedTime}</span> | ||
</Popover> | ||
); | ||
} | ||
} | ||
|
||
SyncStatusPopover.propTypes = { | ||
anchorEl: PropTypes.object, | ||
classes: PropTypes.object, | ||
id: PropTypes.string, | ||
notes: PropTypes.array, | ||
onClose: PropTypes.func.isRequired, | ||
theme: PropTypes.string.isRequired, | ||
unsyncedNoteIds: PropTypes.array.isRequired, | ||
}; | ||
|
||
const mapStateToProps = ({ appState, settings }) => ({ | ||
notes: appState.notes, | ||
theme: settings.theme, | ||
}); | ||
|
||
export default connect(mapStateToProps)(SyncStatusPopover); |
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,61 @@ | ||
.sync-status { | ||
display: flex; | ||
align-items: center; | ||
padding: 1.25em 1.75em; | ||
font-size: .75rem; | ||
line-height: 1; | ||
} | ||
|
||
.sync-status__icon { | ||
width: 18px; | ||
margin-right: .5em; | ||
text-align: center; | ||
} | ||
|
||
.sync-status-popover { | ||
pointer-events: none; | ||
|
||
&.theme-light, | ||
&.theme-dark { | ||
background: transparent; | ||
} | ||
} | ||
|
||
.sync-status-popover__paper { | ||
padding: .5em 1em; | ||
border-radius: $border-radius; | ||
border: 1px solid lighten($gray, 30%); | ||
font-size: .75rem; | ||
|
||
&.has-unsynced-changes { | ||
padding: 1em 1.5em; | ||
} | ||
} | ||
|
||
.sync-status-popover__unsynced { | ||
max-width: 18em; | ||
margin-bottom: .75em; | ||
padding-bottom: 1em; | ||
border-bottom: 1px solid lighten($gray, 30%); | ||
line-height: 1.45; | ||
} | ||
|
||
.sync-status-popover__heading { | ||
margin: .5em 0 0; | ||
font-size: .75rem; | ||
font-weight: $bold; | ||
text-transform: uppercase; | ||
} | ||
|
||
.sync-status-popover__notes { | ||
margin: 1em 0; | ||
padding-left: .5em; | ||
list-style-position: inside; | ||
font-size: .875rem; | ||
|
||
li { | ||
overflow: hidden; | ||
white-space: nowrap; | ||
text-overflow: ellipsis; | ||
} | ||
} |
Oops, something went wrong.