refactor: fix conflicts view when conflicted copies are already in trash (#2339)

This commit is contained in:
Aman Harwara
2023-06-27 22:08:46 +05:30
committed by GitHub
parent f858f31492
commit d3378a704a
7 changed files with 102 additions and 35 deletions

View File

@@ -17,6 +17,7 @@ export * from './Interfaces/DeletedItem'
export * from './Interfaces/EncryptedItem' export * from './Interfaces/EncryptedItem'
export * from './Interfaces/ItemInterface' export * from './Interfaces/ItemInterface'
export * from './Interfaces/TypeCheck' export * from './Interfaces/TypeCheck'
export * from './Interfaces/UnionTypes'
export * from './Mutator/DecryptedItemMutator' export * from './Mutator/DecryptedItemMutator'
export * from './Mutator/DeleteMutator' export * from './Mutator/DeleteMutator'
export * from './Mutator/ItemMutator' export * from './Mutator/ItemMutator'

View File

@@ -185,10 +185,13 @@ export abstract class Collection<
if (this.isDecryptedElement(element)) { if (this.isDecryptedElement(element)) {
const conflictOf = element.content.conflict_of const conflictOf = element.content.conflict_of
if (conflictOf) {
if (conflictOf && !element.content.trashed) {
this.conflictMap.establishRelationship(conflictOf, element.uuid) this.conflictMap.establishRelationship(conflictOf, element.uuid)
} }
const isConflictOfButTrashed = conflictOf && element.content.trashed
const isInConflictMapButIsNotConflictOf = const isInConflictMapButIsNotConflictOf =
!conflictOf && this.conflictMap.getInverseRelationships(element.uuid).length > 0 !conflictOf && this.conflictMap.getInverseRelationships(element.uuid).length > 0
@@ -196,7 +199,7 @@ export abstract class Collection<
this.conflictMap.existsInDirectMap(element.uuid) && this.conflictMap.existsInDirectMap(element.uuid) &&
this.conflictMap.getDirectRelationships(element.uuid).length === 0 this.conflictMap.getDirectRelationships(element.uuid).length === 0
if (isInConflictMapButIsNotConflictOf || isInConflictMapButDoesNotHaveConflicts) { if (isInConflictMapButIsNotConflictOf || isInConflictMapButDoesNotHaveConflicts || isConflictOfButTrashed) {
this.conflictMap.removeFromMap(element.uuid) this.conflictMap.removeFromMap(element.uuid)
} }

View File

@@ -6,12 +6,13 @@ import { ItemCollection } from './ItemCollection'
import { FillItemContent, ItemContent } from '../../../Abstract/Content/ItemContent' import { FillItemContent, ItemContent } from '../../../Abstract/Content/ItemContent'
describe('item collection', () => { describe('item collection', () => {
const createDecryptedPayload = (uuid?: string): DecryptedPayload => { const createDecryptedPayload = (uuid?: string, content?: Partial<NoteContent>): DecryptedPayload => {
return new DecryptedPayload({ return new DecryptedPayload({
uuid: uuid || String(Math.random()), uuid: uuid || String(Math.random()),
content_type: ContentType.Note, content_type: ContentType.Note,
content: FillItemContent<NoteContent>({ content: FillItemContent<NoteContent>({
title: 'foo', title: 'foo',
...content,
}), }),
...PayloadTimestampDefaults(), ...PayloadTimestampDefaults(),
}) })
@@ -33,4 +34,81 @@ describe('item collection', () => {
expect(collection.all()).toHaveLength(1) expect(collection.all()).toHaveLength(1)
}) })
it('should not add conflicted copy to conflictMap if trashed', () => {
const collection = new ItemCollection()
const mainItem = new DecryptedItem(createDecryptedPayload())
const nonTrashedConflictedItem = new DecryptedItem(
createDecryptedPayload(undefined, {
conflict_of: mainItem.uuid,
}),
)
const trashedConflictedItem = new DecryptedItem(
createDecryptedPayload(undefined, {
conflict_of: mainItem.uuid,
trashed: true,
}),
)
collection.set([mainItem, nonTrashedConflictedItem, trashedConflictedItem])
expect(collection.conflictMap.existsInDirectMap(mainItem.uuid)).toBe(true)
expect(collection.conflictMap.getDirectRelationships(mainItem.uuid).includes(nonTrashedConflictedItem.uuid)).toBe(
true,
)
expect(collection.conflictMap.getDirectRelationships(mainItem.uuid).includes(trashedConflictedItem.uuid)).toBe(
false,
)
})
it('should remove conflicted copy from conflictMap if not conflicted anymore', () => {
const collection = new ItemCollection()
const mainItem = new DecryptedItem(createDecryptedPayload())
const conflictedItem = new DecryptedItem(
createDecryptedPayload(undefined, {
conflict_of: mainItem.uuid,
}),
)
collection.set([mainItem, conflictedItem])
expect(collection.conflictMap.existsInInverseMap(conflictedItem.uuid)).toBe(true)
const updatedConflictedItem = new DecryptedItem(
conflictedItem.payload.copy({
content: { conflict_of: undefined } as unknown as jest.Mocked<ItemContent>,
}),
)
collection.set(updatedConflictedItem)
expect(collection.conflictMap.existsInInverseMap(conflictedItem.uuid)).toBe(false)
})
it('should remove conflict parent if it has no conflicts anymore', () => {
const collection = new ItemCollection()
const mainItem = new DecryptedItem(createDecryptedPayload())
const conflictedItem = new DecryptedItem(
createDecryptedPayload(undefined, {
conflict_of: mainItem.uuid,
}),
)
collection.set([mainItem, conflictedItem])
expect(collection.conflictMap.existsInDirectMap(mainItem.uuid)).toBe(true)
const updatedConflictedItem = new DecryptedItem(
conflictedItem.payload.copy({
content: { conflict_of: undefined } as unknown as jest.Mocked<ItemContent>,
}),
)
collection.set([updatedConflictedItem, mainItem])
expect(collection.conflictMap.existsInDirectMap(mainItem.uuid)).toBe(false)
})
}) })

View File

@@ -6,6 +6,7 @@ import { ItemWithTags } from './Search/ItemWithTags'
import { itemMatchesQuery, itemPassesFilters } from './Search/SearchUtilities' import { itemMatchesQuery, itemPassesFilters } from './Search/SearchUtilities'
import { ItemFilter, ReferenceLookupCollection, SearchableDecryptedItem } from './Search/Types' import { ItemFilter, ReferenceLookupCollection, SearchableDecryptedItem } from './Search/Types'
import { FilterDisplayOptions } from './DisplayOptions' import { FilterDisplayOptions } from './DisplayOptions'
import { SystemViewId } from '../../Syncable/SmartView'
export function computeUnifiedFilterForDisplayOptions( export function computeUnifiedFilterForDisplayOptions(
options: FilterDisplayOptions, options: FilterDisplayOptions,
@@ -75,7 +76,10 @@ export function computeFiltersForDisplayOptions(
filters.push((item) => itemMatchesQuery(item, query, collection)) filters.push((item) => itemMatchesQuery(item, query, collection))
} }
if (!viewsPredicate?.keypathIncludesString('conflict_of')) { if (
!viewsPredicate?.keypathIncludesString('conflict_of') &&
!options.views?.some((v) => v.uuid === SystemViewId.TrashedNotes)
) {
filters.push((item) => !item.conflictOf) filters.push((item) => !item.conflictOf)
} }

View File

@@ -18,6 +18,7 @@ import {
ItemsKeyInterface, ItemsKeyInterface,
ItemContent, ItemContent,
DecryptedPayload, DecryptedPayload,
AnyItemInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
export interface ItemsClientInterface { export interface ItemsClientInterface {
@@ -168,5 +169,6 @@ export interface ItemsClientInterface {
iconString?: string, iconString?: string,
): Promise<SmartView> ): Promise<SmartView>
conflictsOf(uuid: string): AnyItemInterface[]
numberOfNotesWithConflicts(): number numberOfNotesWithConflicts(): number
} }

View File

@@ -1415,7 +1415,11 @@ export class ItemManager
}) })
} }
numberOfNotesWithConflicts(): number { public conflictsOf(uuid: string) {
return this.collection.conflictsOf(uuid)
}
public numberOfNotesWithConflicts(): number {
return this.collection.numberOfItemsWithConflicts() return this.collection.numberOfItemsWithConflicts()
} }
} }

View File

@@ -432,36 +432,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.debounceReloadEditorComponent() this.debounceReloadEditorComponent()
}) })
this.removeNoteStreamObserver = this.application.streamItems<SNNote>( this.removeNoteStreamObserver = this.application.streamItems<SNNote>(ContentType.Note, async () => {
ContentType.Note, this.setState({
async ({ inserted, changed, removed }) => { conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
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) { private createComponentViewer(component: SNComponent) {