feat: Added a conflict resolution dialog and a Conflicts view for easier management of conflicts (#2337)

This commit is contained in:
Aman Harwara
2023-06-25 14:27:51 +05:30
committed by GitHub
parent 49d43fd14b
commit e0e9249334
48 changed files with 1201 additions and 94 deletions

View File

@@ -9,7 +9,7 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { log, LoggingDomain } from '@/Logging'
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
import { classNames } from '@standardnotes/utils'
import { classNames, pluralize } from '@standardnotes/utils'
import {
ApplicationEvent,
ComponentArea,
@@ -45,6 +45,10 @@ import { SuperEditorContentId } from '../SuperEditor/Constants'
import { NoteViewController } from './Controller/NoteViewController'
import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor'
import { EditorMargins, EditorMaxWidths } from '../EditorWidthSelectionModal/EditorWidths'
import Button from '../Button/Button'
import ModalOverlay from '../Modal/ModalOverlay'
import NoteConflictResolutionModal from './NoteConflictResolutionModal/NoteConflictResolutionModal'
import Icon from '../Icon/Icon'
const MinimumStatusDuration = 400
@@ -74,6 +78,9 @@ type State = {
updateSavingIndicator?: boolean
editorFeatureIdentifier?: string
noteType?: NoteType
conflictedNotes: SNNote[]
showConflictResolutionModal: boolean
}
class NoteView extends AbstractComponent<NoteViewProps, State> {
@@ -84,6 +91,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
private removeTrashKeyObserver?: () => void
private removeComponentStreamObserver?: () => void
private removeNoteStreamObserver?: () => void
private removeComponentManagerObserver?: () => void
private removeInnerNoteObserver?: () => void
@@ -120,6 +128,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
syncTakingTooLong: false,
editorFeatureIdentifier: this.controller.item.editorIdentifier,
noteType: this.controller.item.noteType,
conflictedNotes: [],
showConflictResolutionModal: false,
}
this.noteViewElementRef = createRef<HTMLDivElement>()
@@ -133,6 +143,9 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.removeComponentStreamObserver?.()
;(this.removeComponentStreamObserver as unknown) = undefined
this.removeNoteStreamObserver?.()
;(this.removeNoteStreamObserver as unknown) = undefined
this.removeInnerNoteObserver?.()
;(this.removeInnerNoteObserver as unknown) = undefined
@@ -418,6 +431,37 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
await this.reloadStackComponents()
this.debounceReloadEditorComponent()
})
this.removeNoteStreamObserver = this.application.streamItems<SNNote>(
ContentType.Note,
async ({ inserted, changed, removed }) => {
const insertedOrChanged = inserted.concat(changed)
for (const note of insertedOrChanged) {
if (note.conflictOf === this.note.uuid && !note.trashed) {
this.setState((state) => ({
conflictedNotes: state.conflictedNotes
.filter((conflictedNote) => conflictedNote.uuid !== note.uuid)
.concat([note]),
}))
} else {
this.setState((state) => {
return {
conflictedNotes: state.conflictedNotes.filter((conflictedNote) => conflictedNote.uuid !== note.uuid),
}
})
}
}
for (const note of removed) {
this.setState((state) => {
return {
conflictedNotes: state.conflictedNotes.filter((conflictedNote) => conflictedNote.uuid !== note.uuid),
}
})
}
},
)
}
private createComponentViewer(component: SNComponent) {
@@ -776,6 +820,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.setState({ plainEditorFocused: false })
}
toggleConflictResolutionModal = () => {
this.setState((state) => ({
showConflictResolutionModal: !state.showConflictResolutionModal,
}))
}
override render() {
if (this.controller.dealloced) {
return null
@@ -803,6 +853,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
? 'component'
: 'plain'
const shouldShowConflictsButton = this.state.conflictedNotes.length > 0
return (
<div aria-label="Note" className="section editor sn-component h-full md:max-h-full" ref={this.noteViewElementRef}>
{this.note && (
@@ -826,7 +878,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
id="editor-title-bar"
className="content-title-bar section-title-bar z-editor-title-bar w-full bg-default pt-4"
>
<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(
'mb-2 flex flex-wrap justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-4 xl:items-center',
shouldShowConflictsButton ? 'items-center' : 'items-start',
)}
>
<div className={classNames(this.state.noteLocked && 'locked', 'flex flex-grow items-center')}>
<MobileItemsListButton />
<div className="title flex-grow overflow-auto">
@@ -850,6 +907,19 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
updateSavingIndicator={this.state.updateSavingIndicator}
/>
</div>
{shouldShowConflictsButton && (
<Button
className="flex items-center"
primary
colorStyle="warning"
small
onClick={this.toggleConflictResolutionModal}
>
<Icon type="merge" size="small" className="mr-2" />
{this.state.conflictedNotes.length}{' '}
{pluralize(this.state.conflictedNotes.length, 'conflict', 'conflicts')}
</Button>
)}
{renderHeaderOptions && (
<div className="note-view-options-buttons flex items-center gap-3">
<LinkedItemsButton
@@ -979,6 +1049,14 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
})}
</div>
</div>
<ModalOverlay isOpen={this.state.showConflictResolutionModal} close={this.toggleConflictResolutionModal}>
<NoteConflictResolutionModal
currentNote={this.note}
conflictedNotes={this.state.conflictedNotes}
close={this.toggleConflictResolutionModal}
/>
</ModalOverlay>
</div>
)
}