feat: new note status indicator (#1679)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { useState } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
export type NoteStatus = {
|
||||
type: 'saving' | 'saved' | 'error'
|
||||
message: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
status: NoteStatus | undefined
|
||||
syncTakingTooLong: boolean
|
||||
}
|
||||
|
||||
const NoteStatusIndicator = ({ status, syncTakingTooLong }: Props) => {
|
||||
const [shouldShowTooltip, setShouldShowTooltip] = useState(false)
|
||||
|
||||
if (!status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
className={classNames(
|
||||
'peer flex h-5 w-5 items-center justify-center rounded-full',
|
||||
status.type === 'saving' && 'bg-contrast',
|
||||
status.type === 'saved' && 'bg-success text-success-contrast',
|
||||
status.type === 'error' && 'bg-danger text-danger-contrast',
|
||||
syncTakingTooLong && 'bg-warning text-warning-contrast',
|
||||
)}
|
||||
onClick={() => setShouldShowTooltip((show) => !show)}
|
||||
onBlur={() => setShouldShowTooltip(false)}
|
||||
aria-describedby={ElementIds.NoteStatusTooltip}
|
||||
>
|
||||
<Icon
|
||||
className={status.type === 'saving' ? 'animate-spin' : ''}
|
||||
type={status.type === 'saved' ? 'check' : status.type === 'error' ? 'warning' : 'sync'}
|
||||
size="small"
|
||||
/>
|
||||
<span className="sr-only">Note sync status</span>
|
||||
</button>
|
||||
<div
|
||||
id={ElementIds.NoteStatusTooltip}
|
||||
className={classNames(
|
||||
shouldShowTooltip ? '' : 'hidden',
|
||||
'absolute top-full right-0 min-w-max translate-x-2 translate-y-1 select-none rounded border border-border bg-default py-1.5 px-3 text-left peer-hover:block peer-focus:block',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'text-sm font-bold',
|
||||
status.type === 'error' && 'text-danger',
|
||||
syncTakingTooLong && 'text-warning',
|
||||
)}
|
||||
>
|
||||
{status.message}
|
||||
</div>
|
||||
{status.desc && <div className="mt-0.5">{status.desc}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteStatusIndicator
|
||||
@@ -41,6 +41,7 @@ import AutoresizingNoteViewTextarea from './AutoresizingTextarea'
|
||||
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
||||
import NoteTagsPanel from '../NoteTags/NoteTagsPanel'
|
||||
import NoteTagsContainer from '../NoteTags/NoteTagsContainer'
|
||||
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
|
||||
const MinimumStatusDuration = 400
|
||||
@@ -48,11 +49,6 @@ const TextareaDebounce = 100
|
||||
const NoteEditingDisabledText = 'Note editing disabled.'
|
||||
const StickyHeaderScrollThresholdInPx = 20
|
||||
|
||||
type NoteStatus = {
|
||||
message?: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
||||
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
|
||||
}
|
||||
@@ -346,6 +342,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
break
|
||||
case ApplicationEvent.LocalDatabaseWriteError:
|
||||
this.showErrorStatus({
|
||||
type: 'error',
|
||||
message: 'Offline Saving Issue',
|
||||
desc: 'Changes not saved',
|
||||
})
|
||||
@@ -503,7 +500,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
showSavingStatus() {
|
||||
this.setStatus({ message: 'Saving…' }, false)
|
||||
this.setStatus({ type: 'saving', message: 'Saving…' }, false)
|
||||
}
|
||||
|
||||
showAllChangesSavedStatus() {
|
||||
@@ -512,6 +509,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
syncTakingTooLong: false,
|
||||
})
|
||||
this.setStatus({
|
||||
type: 'saved',
|
||||
message: 'All changes saved' + (this.application.noAccount() ? ' offline' : ''),
|
||||
})
|
||||
}
|
||||
@@ -519,6 +517,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
showErrorStatus(error?: NoteStatus) {
|
||||
if (!error) {
|
||||
error = {
|
||||
type: 'error',
|
||||
message: 'Sync Unreachable',
|
||||
desc: 'Changes saved offline',
|
||||
}
|
||||
@@ -948,7 +947,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
: '',
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-start justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-0 xl:items-center">
|
||||
<div className="mb-2 flex flex-wrap items-start justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-4 xl:items-center">
|
||||
<div className={classNames(this.state.noteLocked && 'locked', 'flex flex-grow items-center')}>
|
||||
<MobileItemsListButton />
|
||||
<div className="title flex-grow overflow-auto">
|
||||
@@ -966,60 +965,41 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<NoteStatusIndicator status={this.state.noteStatus} syncTakingTooLong={this.state.syncTakingTooLong} />
|
||||
</div>
|
||||
{!this.state.shouldStickyHeader && (
|
||||
<div className="flex flex-row-reverse items-center gap-3 md:flex-col-reverse md:items-end xl:flex-row xl:flex-nowrap xl:items-center">
|
||||
{this.state.noteStatus?.message?.length && (
|
||||
<div id="save-status-container" className={'xl:mr-5 xl:max-w-[16ch]'}>
|
||||
<div id="save-status">
|
||||
<div
|
||||
className={
|
||||
(this.state.syncTakingTooLong ? 'font-bold text-warning ' : '') +
|
||||
(this.state.saveError ? 'font-bold text-danger ' : '') +
|
||||
'message text-xs'
|
||||
}
|
||||
>
|
||||
{this.state.noteStatus?.message}
|
||||
</div>
|
||||
{this.state.noteStatus?.desc && (
|
||||
<div className="desc text-xs">{this.state.noteStatus.desc}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<NoteTagsPanel
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
/>
|
||||
<AttachedFilesButton
|
||||
application={this.application}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
featuresController={this.viewControllerManager.featuresController}
|
||||
filePreviewModalController={this.viewControllerManager.filePreviewModalController}
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
selectionController={this.viewControllerManager.selectionController}
|
||||
/>
|
||||
<ChangeEditorButton
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
<PinNoteButton
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
<NotesOptionsPanel
|
||||
application={this.application}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
historyModalController={this.viewControllerManager.historyModalController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<NoteTagsPanel
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
/>
|
||||
<AttachedFilesButton
|
||||
application={this.application}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
featuresController={this.viewControllerManager.featuresController}
|
||||
filePreviewModalController={this.viewControllerManager.filePreviewModalController}
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
selectionController={this.viewControllerManager.selectionController}
|
||||
/>
|
||||
<ChangeEditorButton
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
<PinNoteButton
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
<NotesOptionsPanel
|
||||
application={this.application}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
historyModalController={this.viewControllerManager.historyModalController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,4 +9,5 @@ export const ElementIds = {
|
||||
NoteTextEditor: 'note-text-editor',
|
||||
NoteTitleEditor: 'note-title-editor',
|
||||
RootId: 'app-group-root',
|
||||
NoteStatusTooltip: 'note-status-tooltip',
|
||||
} as const
|
||||
|
||||
Reference in New Issue
Block a user