From 4030953b00ad609dc2b0f5b66cfd971d449f6121 Mon Sep 17 00:00:00 2001 From: Mo Date: Wed, 19 Oct 2022 14:36:30 -0500 Subject: [PATCH] fix: issue with not being able to unlink a file from a note (#1836) --- .../models/src/Domain/Syncable/File/File.ts | 5 +- .../Domain/Item/ItemRelationshipDirection.ts | 7 ++ .../src/Domain/Item/ItemsClientInterface.ts | 22 +--- packages/services/src/Domain/index.ts | 1 + .../lib/Services/Items/ItemManager.spec.ts | 115 ++++-------------- .../snjs/lib/Services/Items/ItemManager.ts | 93 ++++---------- .../ContentListView/NoteListItem.tsx | 4 +- .../LinkedItems/LinkedItemBubble.tsx | 2 +- .../Controllers/FilesController.ts | 11 +- .../Controllers/LinkingController.tsx | 105 +++++++--------- 10 files changed, 122 insertions(+), 243 deletions(-) create mode 100644 packages/services/src/Domain/Item/ItemRelationshipDirection.ts diff --git a/packages/models/src/Domain/Syncable/File/File.ts b/packages/models/src/Domain/Syncable/File/File.ts index 0a79acc81..b89cf9e94 100644 --- a/packages/models/src/Domain/Syncable/File/File.ts +++ b/packages/models/src/Domain/Syncable/File/File.ts @@ -4,7 +4,8 @@ import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/Dec import { FileMetadata } from './FileMetadata' import { FileProtocolV1 } from './FileProtocolV1' import { SortableItem } from '../../Runtime/Collection/CollectionSort' -import { ConflictStrategy } from '../../Abstract/Item' +import { ConflictStrategy, ItemInterface } from '../../Abstract/Item' +import { ContentType } from '@standardnotes/common' type EncryptedBytesLength = number type DecryptedBytesLength = number @@ -31,6 +32,8 @@ export type FileContentSpecialized = FileContentWithoutSize & FileMetadata & Siz export type FileContent = FileContentSpecialized & ItemContent +export const isFile = (x: ItemInterface): x is FileItem => x.content_type === ContentType.File + export class FileItem extends DecryptedItem implements FileContentWithoutSize, Sizes, FileProtocolV1, FileMetadata, SortableItem diff --git a/packages/services/src/Domain/Item/ItemRelationshipDirection.ts b/packages/services/src/Domain/Item/ItemRelationshipDirection.ts new file mode 100644 index 000000000..95b0addd2 --- /dev/null +++ b/packages/services/src/Domain/Item/ItemRelationshipDirection.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ + +export enum ItemRelationshipDirection { + AReferencesB, + BReferencesA, + NoRelationship, +} diff --git a/packages/services/src/Domain/Item/ItemsClientInterface.ts b/packages/services/src/Domain/Item/ItemsClientInterface.ts index 7e3d22f96..0647912f3 100644 --- a/packages/services/src/Domain/Item/ItemsClientInterface.ts +++ b/packages/services/src/Domain/Item/ItemsClientInterface.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { ContentType, Uuid } from '@standardnotes/common' import { SNNote, @@ -76,9 +78,9 @@ export interface ItemsClientInterface { linkNoteToNote(note: SNNote, otherNote: SNNote): Promise linkFileToFile(file: FileItem, otherFile: FileItem): Promise - unlinkItem( - item: DecryptedItemInterface, - itemToUnlink: DecryptedItemInterface, + unlinkItems( + itemOne: DecryptedItemInterface, + itemTwo: DecryptedItemInterface, ): Promise> /** @@ -115,12 +117,6 @@ export interface ItemsClientInterface { */ getSortedTagsForItem(item: DecryptedItemInterface): SNTag[] - getSortedLinkedFilesForItem(item: DecryptedItemInterface): FileItem[] - getSortedFilesLinkingToItem(item: DecryptedItemInterface): FileItem[] - - getSortedLinkedNotesForItem(item: DecryptedItemInterface): SNNote[] - getSortedNotesLinkingToItem(item: DecryptedItemInterface): SNNote[] - isSmartViewTitle(title: string): boolean getSmartViews(): SmartView[] @@ -152,12 +148,4 @@ export interface ItemsClientInterface { * @returns Whether the item is a template (unmanaged) */ isTemplateItem(item: DecryptedItemInterface): boolean - - /** - * @returns `'direct'` if `itemOne` has the reference to `itemTwo`, `'indirect'` if `itemTwo` has the reference to `itemOne`, `'unlinked'` if neither reference each other - */ - relationshipTypeForItems( - itemOne: DecryptedItemInterface, - itemTwo: DecryptedItemInterface, - ): 'direct' | 'indirect' | 'unlinked' } diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 0b8a43b93..c55046011 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -54,6 +54,7 @@ export * from './Item/ItemCounterInterface' export * from './Item/ItemManagerInterface' export * from './Item/ItemsClientInterface' export * from './Item/ItemsServerInterface' +export * from './Item/ItemRelationshipDirection' export * from './Mutator/MutatorClientInterface' export * from './Payloads/PayloadManagerInterface' export * from './Preferences/PreferenceServiceInterface' diff --git a/packages/snjs/lib/Services/Items/ItemManager.spec.ts b/packages/snjs/lib/Services/Items/ItemManager.spec.ts index ed5c1b2c8..b8311aace 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.spec.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.spec.ts @@ -1,5 +1,5 @@ import { ContentType } from '@standardnotes/common' -import { InternalEventBusInterface } from '@standardnotes/services' +import { InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services' import { ItemManager } from './ItemManager' import { PayloadManager } from '../Payloads/PayloadManager' import { UuidGenerator } from '@standardnotes/utils' @@ -784,21 +784,6 @@ describe('itemManager', () => { expect(references).toHaveLength(0) }) - it('should get files linked with note', async () => { - itemManager = createService() - const note = createNoteWithTitle('invoices') - const file = createFile('invoice_1.pdf') - const secondFile = createFile('unrelated-file.xlsx') - await itemManager.insertItems([note, file, secondFile]) - - await itemManager.associateFileWithNote(file, note) - - const filesAssociatedWithNote = itemManager.getSortedFilesLinkingToItem(note) - - expect(filesAssociatedWithNote).toHaveLength(1) - expect(filesAssociatedWithNote[0].uuid).toBe(file.uuid) - }) - it('should link note to note', async () => { itemManager = createService() const note = createNoteWithTitle('research') @@ -834,101 +819,49 @@ describe('itemManager', () => { const firstNoteLinkedToSecond = await itemManager.linkNoteToNote(firstNote, secondNote) - const relationshipOfFirstNoteToSecond = itemManager.relationshipTypeForItems(firstNoteLinkedToSecond, secondNote) - const relationshipOfSecondNoteToFirst = itemManager.relationshipTypeForItems(secondNote, firstNoteLinkedToSecond) - const relationshipOfFirstNoteToUnlinked = itemManager.relationshipTypeForItems( + const relationshipOfFirstNoteToSecond = itemManager.relationshipDirectionBetweenItems( + firstNoteLinkedToSecond, + secondNote, + ) + const relationshipOfSecondNoteToFirst = itemManager.relationshipDirectionBetweenItems( + secondNote, + firstNoteLinkedToSecond, + ) + const relationshipOfFirstNoteToUnlinked = itemManager.relationshipDirectionBetweenItems( firstNoteLinkedToSecond, unlinkedNote, ) - expect(relationshipOfFirstNoteToSecond).toBe('direct') - expect(relationshipOfSecondNoteToFirst).toBe('indirect') - expect(relationshipOfFirstNoteToUnlinked).toBe('unlinked') + expect(relationshipOfFirstNoteToSecond).toBe(ItemRelationshipDirection.AReferencesB) + expect(relationshipOfSecondNoteToFirst).toBe(ItemRelationshipDirection.BReferencesA) + expect(relationshipOfFirstNoteToUnlinked).toBe(ItemRelationshipDirection.NoRelationship) }) - it('should unlink itemToUnlink from item', async () => { + it('should unlink itemOne from itemTwo if relation is direct', async () => { itemManager = createService() const note = createNoteWithTitle('Note 1') const note2 = createNoteWithTitle('Note 2') await itemManager.insertItems([note, note2]) const linkedItem = await itemManager.linkNoteToNote(note, note2) - const unlinkedItem = await itemManager.unlinkItem(linkedItem, note2) + const unlinkedItem = await itemManager.unlinkItems(linkedItem, note2) const references = unlinkedItem.references + expect(unlinkedItem.uuid).toBe(note.uuid) expect(references).toHaveLength(0) }) - it('should get all linked files for item', async () => { + it('should unlink itemTwo from itemOne if relation is indirect', async () => { itemManager = createService() - const file = createFile('A1') - const file2 = createFile('B2') - const file3 = createFile('C3') + const note = createNoteWithTitle('Note 1') + const note2 = createNoteWithTitle('Note 2') + await itemManager.insertItems([note, note2]) - await itemManager.insertItems([file, file2, file3]) + const linkedItem = await itemManager.linkNoteToNote(note, note2) + const changedItem = await itemManager.unlinkItems(linkedItem, note2) - await itemManager.linkFileToFile(file, file3) - await itemManager.linkFileToFile(file, file2) - - const sortedFilesForItem = itemManager.getSortedLinkedFilesForItem(file) - - expect(sortedFilesForItem).toHaveLength(2) - expect(sortedFilesForItem[0].uuid).toEqual(file2.uuid) - expect(sortedFilesForItem[1].uuid).toEqual(file3.uuid) - }) - - it('should get all files linking to item', async () => { - itemManager = createService() - const baseFile = createFile('file') - const fileToLink1 = createFile('A1') - const fileToLink2 = createFile('B2') - - await itemManager.insertItems([baseFile, fileToLink1, fileToLink2]) - - await itemManager.linkFileToFile(fileToLink2, baseFile) - await itemManager.linkFileToFile(fileToLink1, baseFile) - - const sortedFilesForItem = itemManager.getSortedFilesLinkingToItem(baseFile) - - expect(sortedFilesForItem).toHaveLength(2) - expect(sortedFilesForItem[0].uuid).toEqual(fileToLink1.uuid) - expect(sortedFilesForItem[1].uuid).toEqual(fileToLink2.uuid) - }) - - it('should get all linked notes for item', async () => { - itemManager = createService() - const baseNote = createNoteWithTitle('note') - const noteToLink1 = createNoteWithTitle('A1') - const noteToLink2 = createNoteWithTitle('B2') - - await itemManager.insertItems([baseNote, noteToLink1, noteToLink2]) - - await itemManager.linkNoteToNote(baseNote, noteToLink2) - await itemManager.linkNoteToNote(baseNote, noteToLink1) - - const sortedFilesForItem = itemManager.getSortedLinkedNotesForItem(baseNote) - - expect(sortedFilesForItem).toHaveLength(2) - expect(sortedFilesForItem[0].uuid).toEqual(noteToLink1.uuid) - expect(sortedFilesForItem[1].uuid).toEqual(noteToLink2.uuid) - }) - - it('should get all notes linking to item', async () => { - itemManager = createService() - const baseNote = createNoteWithTitle('note') - const noteToLink1 = createNoteWithTitle('A1') - const noteToLink2 = createNoteWithTitle('B2') - - await itemManager.insertItems([baseNote, noteToLink1, noteToLink2]) - - await itemManager.linkNoteToNote(noteToLink2, baseNote) - await itemManager.linkNoteToNote(noteToLink1, baseNote) - - const sortedFilesForItem = itemManager.getSortedNotesLinkingToItem(baseNote) - - expect(sortedFilesForItem).toHaveLength(2) - expect(sortedFilesForItem[0].uuid).toEqual(noteToLink1.uuid) - expect(sortedFilesForItem[1].uuid).toEqual(noteToLink2.uuid) + expect(changedItem.uuid).toBe(note.uuid) + expect(changedItem.references).toHaveLength(0) }) }) }) diff --git a/packages/snjs/lib/Services/Items/ItemManager.ts b/packages/snjs/lib/Services/Items/ItemManager.ts index a7bb53c49..a5db04adc 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.ts @@ -7,7 +7,7 @@ import { UuidString } from '../../Types/UuidString' import * as Models from '@standardnotes/models' import * as Services from '@standardnotes/services' import { PayloadManagerChangeData } from '../Payloads' -import { DiagnosticInfo, ItemsClientInterface } from '@standardnotes/services' +import { DiagnosticInfo, ItemsClientInterface, ItemRelationshipDirection } from '@standardnotes/services' import { ApplicationDisplayOptions } from '@Lib/Application/Options/OptionalOptions' import { CollectionSort, DecryptedItemInterface, ItemContent } from '@standardnotes/models' @@ -1169,12 +1169,18 @@ export class ItemManager }) } - public async unlinkItem( - item: DecryptedItemInterface, - itemToUnlink: DecryptedItemInterface, - ) { - return this.changeItem(item, (mutator) => { - mutator.removeItemAsRelationship(itemToUnlink) + public async unlinkItems(itemA: DecryptedItemInterface, itemB: DecryptedItemInterface) { + const relationshipDirection = this.relationshipDirectionBetweenItems(itemA, itemB) + + if (relationshipDirection === ItemRelationshipDirection.NoRelationship) { + throw new Error('Trying to unlink already unlinked items') + } + + const itemToChange = relationshipDirection === ItemRelationshipDirection.AReferencesB ? itemA : itemB + const itemToRemove = itemToChange === itemA ? itemB : itemA + + return this.changeItem(itemToChange, (mutator) => { + mutator.removeItemAsRelationship(itemToRemove) }) } @@ -1192,54 +1198,6 @@ export class ItemManager ) } - public getSortedLinkedFilesForItem(item: DecryptedItemInterface): Models.FileItem[] { - if (this.isTemplateItem(item)) { - return [] - } - - const filesReferencedByItem = this.referencesForItem(item).filter( - (ref) => ref.content_type === ContentType.File, - ) as Models.FileItem[] - - return naturalSort(filesReferencedByItem, 'title') - } - - public getSortedFilesLinkingToItem(item: DecryptedItemInterface): Models.FileItem[] { - if (this.isTemplateItem(item)) { - return [] - } - - const filesReferencingItem = this.itemsReferencingItem(item).filter( - (ref) => ref.content_type === ContentType.File, - ) as Models.FileItem[] - - return naturalSort(filesReferencingItem, 'title') - } - - public getSortedLinkedNotesForItem(item: DecryptedItemInterface): Models.SNNote[] { - if (this.isTemplateItem(item)) { - return [] - } - - const notesReferencedByItem = this.referencesForItem(item).filter( - (ref) => ref.content_type === ContentType.Note, - ) as Models.SNNote[] - - return naturalSort(notesReferencedByItem, 'title') - } - - public getSortedNotesLinkingToItem(item: Models.DecryptedItemInterface): Models.SNNote[] { - if (this.isTemplateItem(item)) { - return [] - } - - const notesReferencingItem = this.itemsReferencingItem(item).filter( - (ref) => ref.content_type === ContentType.Note, - ) as Models.SNNote[] - - return naturalSort(notesReferencingItem, 'title') - } - public async createTag(title: string, parentItemToLookupUuidFor?: Models.SNTag): Promise { const newTag = await this.createItem( ContentType.Tag, @@ -1433,21 +1391,18 @@ export class ItemManager return this.findAnyItems(uuids) as (Models.DecryptedItemInterface | Models.DeletedItemInterface)[] } - /** - * @returns `'direct'` if `itemOne` has the reference to `itemTwo`, `'indirect'` if `itemTwo` has the reference to `itemOne`, `'unlinked'` if neither reference each other - */ - public relationshipTypeForItems( - itemOne: Models.DecryptedItemInterface, - itemTwo: Models.DecryptedItemInterface, - ): 'direct' | 'indirect' | 'unlinked' { - const itemOneReferencesItemTwo = this.isTemplateItem(itemOne) - ? false - : !!this.referencesForItem(itemOne).find((reference) => reference.uuid === itemTwo.uuid) - const itemTwoReferencesItemOne = this.isTemplateItem(itemTwo) - ? false - : !!this.referencesForItem(itemTwo).find((reference) => reference.uuid === itemOne.uuid) + public relationshipDirectionBetweenItems( + itemA: Models.DecryptedItemInterface, + itemB: Models.DecryptedItemInterface, + ): ItemRelationshipDirection { + const itemAReferencesItemB = !!itemA.references.find((reference) => reference.uuid === itemB.uuid) + const itemBReferencesItemA = !!itemB.references.find((reference) => reference.uuid === itemA.uuid) - return itemOneReferencesItemTwo ? 'direct' : itemTwoReferencesItemOne ? 'indirect' : 'unlinked' + return itemAReferencesItemB + ? ItemRelationshipDirection.AReferencesB + : itemBReferencesItemA + ? ItemRelationshipDirection.BReferencesA + : ItemRelationshipDirection.NoRelationship } override getDiagnostics(): Promise { diff --git a/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx b/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx index a289bd1b5..7119e9333 100644 --- a/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx @@ -1,5 +1,5 @@ import { PLAIN_EDITOR_NAME } from '@/Constants/Constants' -import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs' +import { isFile, sanitizeHtmlString, SNNote } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useRef } from 'react' import Icon from '@/Components/Icon/Icon' @@ -32,7 +32,7 @@ const NoteListItem: FunctionComponent = ({ const editorForNote = application.componentManager.editorForNote(item as SNNote) const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type) - const hasFiles = application.items.getSortedFilesLinkingToItem(item).length > 0 + const hasFiles = application.items.itemsReferencingItem(item).filter(isFile).length > 0 const openNoteContextMenu = (posX: number, posY: number) => { notesController.setContextMenuOpen(false) diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx index 34671f94c..1b85cf56a 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx @@ -103,7 +103,7 @@ const LinkedItemBubble = ({ {tagTitle && {tagTitle.titlePrefix}} - {link.relationWithSelectedItem === 'indirect' && link.item.content_type !== ContentType.Tag && ( + {link.type === 'linked-by' && link.item.content_type !== ContentType.Tag && ( Linked By: )} {link.item.title} diff --git a/packages/web/src/javascripts/Controllers/FilesController.ts b/packages/web/src/javascripts/Controllers/FilesController.ts index 26617c3e5..3643c06b4 100644 --- a/packages/web/src/javascripts/Controllers/FilesController.ts +++ b/packages/web/src/javascripts/Controllers/FilesController.ts @@ -14,7 +14,14 @@ import { ClassicFileSaver, parseFileName, } from '@standardnotes/filepicker' -import { ChallengeReason, ClientDisplayableError, ContentType, FileItem, InternalEventBus } from '@standardnotes/snjs' +import { + ChallengeReason, + ClientDisplayableError, + ContentType, + FileItem, + InternalEventBus, + isFile, +} from '@standardnotes/snjs' import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/toast' import { action, makeObservable, observable, reaction } from 'mobx' import { WebApplication } from '../Application/Application' @@ -99,7 +106,7 @@ export class FilesController extends AbstractViewController { reloadAttachedFiles = () => { const note = this.notesController.firstSelectedNote if (note) { - this.attachedFiles = this.application.items.getSortedFilesLinkingToItem(note) + this.attachedFiles = this.application.items.itemsReferencingItem(note).filter(isFile) } } diff --git a/packages/web/src/javascripts/Controllers/LinkingController.tsx b/packages/web/src/javascripts/Controllers/LinkingController.tsx index c3a96cabb..b5c151799 100644 --- a/packages/web/src/javascripts/Controllers/LinkingController.tsx +++ b/packages/web/src/javascripts/Controllers/LinkingController.tsx @@ -10,12 +10,13 @@ import { IconType, InternalEventBus, ItemContent, - ItemsClientInterface, naturalSort, NoteViewController, PrefKey, SNNote, SNTag, + isFile, + isNote, } from '@standardnotes/snjs' import { action, computed, makeObservable, observable } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' @@ -27,12 +28,10 @@ import { SubscriptionController } from './Subscription/SubscriptionController' export type LinkableItem = DecryptedItemInterface -type RelationWithSelectedItem = ReturnType - export type ItemLink = { id: string item: ItemType - relationWithSelectedItem: RelationWithSelectedItem + type: 'linked' | 'linked-by' } export class LinkingController extends AbstractViewController { @@ -143,68 +142,68 @@ export class LinkingController extends AbstractViewController { this.reloadNotesLinkingToItem() } - createLinkFromItem = ( - item: ItemType, - relation?: RelationWithSelectedItem, - ): ItemLink => { - const relationWithSelectedItem = relation ? relation : this.itemRelationshipWithSelectedItem(item) - + createLinkFromItem = (itemA: I, type: 'linked' | 'linked-by'): ItemLink => { return { - id: `${item.uuid}-${relationWithSelectedItem}`, - item, - relationWithSelectedItem, + id: `${itemA.uuid}-${type}`, + item: itemA, + type, } } reloadLinkedFiles() { - if (!this.activeItem) { + if (!this.activeItem || this.application.items.isTemplateItem(this.activeItem)) { return } - const isActiveItemAFile = this.activeItem instanceof FileItem + const referencesOfActiveItem = naturalSort( + this.application.items.referencesForItem(this.activeItem).filter(isFile), + 'title', + ) - const linkedFiles = this.application.items - .getSortedLinkedFilesForItem(this.activeItem) - .map((item) => this.createLinkFromItem(item, isActiveItemAFile ? 'direct' : 'indirect')) + const referencingActiveItem = naturalSort( + this.application.items.itemsReferencingItem(this.activeItem).filter(isFile), + 'title', + ) - const filesLinkingToActiveItem = this.application.items - .getSortedFilesLinkingToItem(this.activeItem) - .map((item) => this.createLinkFromItem(item, isActiveItemAFile ? 'indirect' : 'direct')) - - if (isActiveItemAFile) { - this.linkedFiles = linkedFiles - this.filesLinkingToActiveItem = filesLinkingToActiveItem + if (this.activeItem.content_type === ContentType.File) { + this.linkedFiles = referencesOfActiveItem.map((item) => this.createLinkFromItem(item, 'linked')) + this.filesLinkingToActiveItem = referencingActiveItem.map((item) => this.createLinkFromItem(item, 'linked-by')) } else { - this.linkedFiles = filesLinkingToActiveItem - this.filesLinkingToActiveItem = linkedFiles + this.linkedFiles = referencingActiveItem.map((item) => this.createLinkFromItem(item, 'linked')) + this.filesLinkingToActiveItem = referencesOfActiveItem.map((item) => this.createLinkFromItem(item, 'linked-by')) } } reloadLinkedTags() { - if (this.activeItem) { - const tags = this.application.items - .getSortedTagsForItem(this.activeItem) - .map((item) => this.createLinkFromItem(item)) - this.tags = tags + if (!this.activeItem || this.application.items.isTemplateItem(this.activeItem)) { + return } + + this.tags = this.application.items + .getSortedTagsForItem(this.activeItem) + .map((item) => this.createLinkFromItem(item, 'linked')) } reloadLinkedNotes() { - if (this.activeItem) { - const notes = this.application.items - .getSortedLinkedNotesForItem(this.activeItem) - .map((item) => this.createLinkFromItem(item, 'direct')) - this.notesLinkedToItem = notes + if (!this.activeItem || this.application.items.isTemplateItem(this.activeItem)) { + return } + + this.notesLinkedToItem = naturalSort( + this.application.items.referencesForItem(this.activeItem).filter(isNote), + 'title', + ).map((item) => this.createLinkFromItem(item, 'linked')) } reloadNotesLinkingToItem() { - if (this.activeItem) { - const notes = this.application.items - .getSortedNotesLinkingToItem(this.activeItem) - .map((item) => this.createLinkFromItem(item, 'indirect')) - this.notesLinkingToActiveItem = notes + if (!this.activeItem) { + return } + + this.notesLinkingToActiveItem = naturalSort( + this.application.items.itemsReferencingItem(this.activeItem).filter(isNote), + 'title', + ).map((item) => this.createLinkFromItem(item, 'linked-by')) } getTitleForLinkedTag = (item: LinkableItem) => { @@ -263,16 +262,6 @@ export class LinkingController extends AbstractViewController { return undefined } - itemRelationshipWithSelectedItem = (item: LinkableItem) => { - const activeItem = this.activeItem - - if (!activeItem) { - throw new Error('No active item available') - } - - return this.application.items.relationshipTypeForItems(activeItem, item) - } - unlinkItemFromSelectedItem = async (itemToUnlink: ItemLink) => { const selectedItem = this.selectionController.firstSelectedItem @@ -280,13 +269,7 @@ export class LinkingController extends AbstractViewController { return } - const selectedItemReferencesItemToUnlink = itemToUnlink.relationWithSelectedItem === 'direct' - - if (selectedItemReferencesItemToUnlink) { - await this.application.items.unlinkItem(selectedItem, itemToUnlink.item) - } else { - await this.application.items.unlinkItem(itemToUnlink.item, selectedItem) - } + await this.application.items.unlinkItems(selectedItem, itemToUnlink.item) void this.application.sync.sync() this.reloadAllLinks() @@ -404,9 +387,11 @@ export class LinkingController extends AbstractViewController { const linkedResults = searchResults .filter(isAlreadyLinked) .slice(0, 20) - .map((item) => this.createLinkFromItem(item)) + .map((item) => this.createLinkFromItem(item, 'linked')) + const isResultExistingTag = (result: DecryptedItemInterface) => result.content_type === ContentType.Tag && result.title === searchQuery + const shouldShowCreateTag = !linkedResults.find((link) => isResultExistingTag(link.item)) && !unlinkedResults.find(isResultExistingTag)