refactor: item linking (#1781)

This commit is contained in:
Aman Harwara
2022-10-12 21:57:51 +05:30
committed by GitHub
parent 2b89ad488f
commit 81532f2f20
12 changed files with 292 additions and 134 deletions

View File

@@ -68,7 +68,7 @@ export const useFiles = ({ note }: Props) => {
const filesService = application.getFilesService()
const reloadAttachedFiles = useCallback(() => {
setAttachedFiles(application.items.getSortedFilesForItem(note).sort(filesService.sortByName))
setAttachedFiles(application.items.getSortedFilesLinkingToItem(note).sort(filesService.sortByName))
}, [application.items, filesService.sortByName, note])
const reloadAllFiles = useCallback(() => {

View File

@@ -139,7 +139,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
setAttachedFilesLength(0)
return
}
setAttachedFilesLength(application.items.getSortedFilesForItem(note).length)
setAttachedFilesLength(application.items.getSortedFilesLinkingToItem(note).length)
}, [application, note])
useEffect(() => {
@@ -147,7 +147,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
return
}
const removeFilesObserver = application.streamItems(ContentType.File, () => {
setAttachedFilesLength(application.items.getSortedFilesForItem(note).length)
setAttachedFilesLength(application.items.getSortedFilesLinkingToItem(note).length)
})
return () => {
removeFilesObserver()

View File

@@ -114,7 +114,9 @@ export interface ItemsClientInterface {
* @returns Array containing tags associated with an item
*/
getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): SNTag[]
getSortedFilesForItem(item: DecryptedItemInterface<ItemContent>): FileItem[]
getSortedLinkedFilesForItem(item: DecryptedItemInterface<ItemContent>): FileItem[]
getSortedFilesLinkingToItem(item: DecryptedItemInterface<ItemContent>): FileItem[]
getSortedLinkedNotesForItem(item: DecryptedItemInterface<ItemContent>): SNNote[]
getSortedNotesLinkingToItem(item: DecryptedItemInterface<ItemContent>): SNNote[]

View File

@@ -805,7 +805,7 @@ describe('itemManager', () => {
await itemManager.associateFileWithNote(file, note)
const filesAssociatedWithNote = itemManager.getSortedFilesForItem(note)
const filesAssociatedWithNote = itemManager.getSortedFilesLinkingToItem(note)
expect(filesAssociatedWithNote).toHaveLength(1)
expect(filesAssociatedWithNote[0].uuid).toBe(file.uuid)
@@ -873,20 +873,38 @@ describe('itemManager', () => {
it('should get all linked files for item', async () => {
itemManager = createService()
const note = createNote('note')
const file = createFile('A1')
const file2 = createFile('B2')
const file3 = createFile('C3')
await itemManager.insertItems([note, file, file2])
await itemManager.insertItems([file, file2, file3])
await itemManager.associateFileWithNote(file2, note)
await itemManager.associateFileWithNote(file, note)
await itemManager.linkFileToFile(file, file3)
await itemManager.linkFileToFile(file, file2)
const sortedFilesForItem = itemManager.getSortedFilesForItem(note)
const sortedFilesForItem = itemManager.getSortedLinkedFilesForItem(file)
expect(sortedFilesForItem).toHaveLength(2)
expect(sortedFilesForItem[0].uuid).toEqual(file.uuid)
expect(sortedFilesForItem[1].uuid).toEqual(file2.uuid)
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 () => {

View File

@@ -1192,7 +1192,19 @@ export class ItemManager
)
}
public getSortedFilesForItem(item: DecryptedItemInterface<ItemContent>): Models.FileItem[] {
public getSortedLinkedFilesForItem(item: DecryptedItemInterface<ItemContent>): 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<ItemContent>): Models.FileItem[] {
if (this.isTemplateItem(item)) {
return []
}
@@ -1200,11 +1212,8 @@ export class ItemManager
const filesReferencingItem = this.itemsReferencingItem(item).filter(
(ref) => ref.content_type === ContentType.File,
) as Models.FileItem[]
const filesReferencedByItem = this.referencesForItem(item).filter(
(ref) => ref.content_type === ContentType.File,
) as Models.FileItem[]
return naturalSort(filesReferencingItem.concat(filesReferencedByItem), 'title')
return naturalSort(filesReferencingItem, 'title')
}
public getSortedLinkedNotesForItem(item: DecryptedItemInterface<ItemContent>): Models.SNNote[] {

View File

@@ -32,7 +32,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
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.getSortedFilesForItem(item).length > 0
const hasFiles = application.items.getSortedFilesLinkingToItem(item).length > 0
const openNoteContextMenu = (posX: number, posY: number) => {
notesController.setContextMenuOpen(false)

View File

@@ -5,6 +5,7 @@ import * as icons from '@standardnotes/icons'
export const ICONS = {
'account-circle': icons.AccountCircleIcon,
'arrow-left': icons.ArrowLeftIcon,
'arrow-right': icons.ArrowRightIcon,
'arrows-sort-down': icons.ArrowsSortDownIcon,
'arrows-sort-up': icons.ArrowsSortUpIcon,
'attachment-file': icons.AttachmentFileIcon,

View File

@@ -1,24 +1,26 @@
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import { ItemLink, LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { KeyboardKey } from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite'
import { KeyboardEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react'
import { ContentType } from '@standardnotes/snjs'
import Icon from '../Icon/Icon'
type Props = {
item: LinkableItem
link: ItemLink
getItemIcon: LinkingController['getLinkedItemIcon']
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
activateItem: (item: LinkableItem) => Promise<void>
unlinkItem: (item: LinkableItem) => void
unlinkItem: LinkingController['unlinkItemFromSelectedItem']
focusPreviousItem: () => void
focusNextItem: () => void
focusedId: string | undefined
setFocusedId: (id: string) => void
isBidirectional: boolean
}
const LinkedItemBubble = ({
item,
link,
getItemIcon,
getTitleForLinkedTag,
activateItem,
@@ -27,6 +29,7 @@ const LinkedItemBubble = ({
focusNextItem,
focusedId,
setFocusedId,
isBidirectional,
}: Props) => {
const ref = useRef<HTMLButtonElement>(null)
@@ -36,8 +39,8 @@ const LinkedItemBubble = ({
const [wasClicked, setWasClicked] = useState(false)
const handleFocus = () => {
if (focusedId !== item.uuid) {
setFocusedId(item.uuid)
if (focusedId !== link.id) {
setFocusedId(link.id)
}
setShowUnlinkButton(true)
}
@@ -50,7 +53,7 @@ const LinkedItemBubble = ({
const onClick: MouseEventHandler = (event) => {
if (wasClicked && event.target !== unlinkButtonRef.current) {
setWasClicked(false)
void activateItem(item)
void activateItem(link.item)
} else {
setWasClicked(true)
}
@@ -58,14 +61,14 @@ const LinkedItemBubble = ({
const onUnlinkClick: MouseEventHandler = (event) => {
event.stopPropagation()
unlinkItem(item)
unlinkItem(link)
}
const onKeyDown: KeyboardEventHandler = (event) => {
switch (event.key) {
case KeyboardKey.Backspace: {
focusPreviousItem()
unlinkItem(item)
unlinkItem(link)
break
}
case KeyboardKey.Left:
@@ -77,29 +80,34 @@ const LinkedItemBubble = ({
}
}
const [icon, iconClassName] = getItemIcon(item)
const tagTitle = getTitleForLinkedTag(item)
const [icon, iconClassName] = getItemIcon(link.item)
const tagTitle = getTitleForLinkedTag(link.item)
useEffect(() => {
if (item.uuid === focusedId) {
if (link.id === focusedId) {
ref.current?.focus()
}
}, [focusedId, item.uuid])
}, [focusedId, link.id])
return (
<button
ref={ref}
className="flex h-6 cursor-pointer items-center rounded border-0 bg-passive-4-opacity-variant py-2 pl-1 pr-2 text-xs text-text hover:bg-contrast focus:bg-contrast"
className="group flex h-6 cursor-pointer items-center rounded border-0 bg-passive-4-opacity-variant py-2 pl-1 pr-2 text-xs text-text hover:bg-contrast focus:bg-contrast"
onFocus={handleFocus}
onBlur={onBlur}
onClick={onClick}
title={tagTitle ? tagTitle.longTitle : item.title}
title={tagTitle ? tagTitle.longTitle : link.item.title}
onKeyDown={onKeyDown}
>
<Icon type={icon} className={classNames('mr-1 flex-shrink-0', iconClassName)} size="small" />
<span className="max-w-290px overflow-hidden overflow-ellipsis whitespace-nowrap">
<span className="max-w-290px flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap">
{tagTitle && <span className="text-passive-1">{tagTitle.titlePrefix}</span>}
{item.title}
<span className="flex items-center gap-1">
{link.relationWithSelectedItem === 'indirect' && link.item.content_type !== ContentType.Tag && (
<span className={!isBidirectional ? 'hidden group-focus:block' : ''}>Linked By:</span>
)}
{link.item.title}
</span>
</span>
{showUnlinkButton && (
<a

View File

@@ -1,6 +1,6 @@
import { observer } from 'mobx-react-lite'
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import { ItemLink, LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import LinkedItemBubble from './LinkedItemBubble'
import { useCallback, useState } from 'react'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
@@ -14,8 +14,9 @@ type Props = {
const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
const { toggleAppPane } = useResponsiveAppPane()
const {
allLinkedItems,
notesLinkingToItem,
allItemLinks,
notesLinkingToActiveItem,
filesLinkingToActiveItem,
unlinkItemFromSelectedItem: unlinkItem,
getTitleForLinkedTag,
getLinkedItemIcon: getItemIcon,
@@ -23,7 +24,13 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
} = linkingController
const [focusedId, setFocusedId] = useState<string>()
const focusableIds = allLinkedItems.map((item) => item.uuid).concat([ElementIds.ItemLinkAutocompleteInput])
const focusableIds = allItemLinks
.map((link) => link.id)
.concat(
notesLinkingToActiveItem.map((link) => link.id),
filesLinkingToActiveItem.map((link) => link.id),
[ElementIds.ItemLinkAutocompleteInput],
)
const focusPreviousItem = useCallback(() => {
const currentFocusedIndex = focusableIds.findIndex((id) => id === focusedId)
@@ -53,27 +60,38 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
[activateItem, toggleAppPane],
)
const isItemBidirectionallyLinked = (link: ItemLink) => {
const existsInAllItemLinks = !!allItemLinks.find((item) => link.item.uuid === item.item.uuid)
const existsInNotesLinkingToItem = !!notesLinkingToActiveItem.find((item) => link.item.uuid === item.item.uuid)
return existsInAllItemLinks && existsInNotesLinkingToItem
}
return (
<div
className={classNames(
'hidden min-w-80 max-w-full flex-wrap items-center gap-2 bg-transparent md:-mr-2 md:flex',
allLinkedItems.length || notesLinkingToItem.length ? 'mt-1' : 'mt-0.5',
allItemLinks.length || notesLinkingToActiveItem.length ? 'mt-1' : 'mt-0.5',
)}
>
{allLinkedItems.concat(notesLinkingToItem).map((item) => (
<LinkedItemBubble
item={item}
key={item.uuid}
getItemIcon={getItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
activateItem={activateItemAndTogglePane}
unlinkItem={unlinkItem}
focusPreviousItem={focusPreviousItem}
focusNextItem={focusNextItem}
focusedId={focusedId}
setFocusedId={setFocusedId}
/>
))}
{allItemLinks
.concat(notesLinkingToActiveItem)
.concat(filesLinkingToActiveItem)
.map((link) => (
<LinkedItemBubble
link={link}
key={link.id}
getItemIcon={getItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
activateItem={activateItemAndTogglePane}
unlinkItem={unlinkItem}
focusPreviousItem={focusPreviousItem}
focusNextItem={focusNextItem}
focusedId={focusedId}
setFocusedId={setFocusedId}
isBidirectional={isItemBidirectionallyLinked(link)}
/>
))}
<ItemLinkAutocompleteInput
focusedId={focusedId}
linkingController={linkingController}

View File

@@ -33,7 +33,7 @@ const LinkedItemsSectionItem = ({
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
item: LinkableItem
searchQuery?: string
unlinkItem: LinkingController['unlinkItemFromSelectedItem']
unlinkItem: () => void
handleFileAction: FilesController['handleFileAction']
}) => {
const menuButtonRef = useRef<HTMLButtonElement>(null)
@@ -85,7 +85,7 @@ const LinkedItemsSectionItem = ({
</div>
) : (
<button
className="flex flex-grow items-center justify-between gap-4 py-2 pl-3 pr-12 text-sm hover:bg-info-backdrop focus:bg-info-backdrop"
className="flex max-w-full flex-grow items-center justify-between gap-4 py-2 pl-3 pr-12 text-sm hover:bg-info-backdrop focus:bg-info-backdrop"
onClick={() => activateItem(item)}
onContextMenu={(event) => {
event.preventDefault()
@@ -118,7 +118,7 @@ const LinkedItemsSectionItem = ({
<MenuItem
type={MenuItemType.IconButton}
onClick={() => {
unlinkItem(item)
unlinkItem()
toggleMenu()
}}
>
@@ -166,10 +166,11 @@ const LinkedItemsPanel = ({
}) => {
const {
tags,
files,
linkedFiles,
filesLinkingToActiveItem,
notesLinkedToItem,
notesLinkingToItem,
allLinkedItems,
notesLinkingToActiveItem,
allItemLinks: allLinkedItems,
getTitleForLinkedTag,
getLinkedItemIcon,
getSearchResults,
@@ -196,7 +197,7 @@ const LinkedItemsPanel = ({
<form
className={classNames(
'sticky top-0 z-10 bg-default px-2.5 pt-2.5',
allLinkedItems.length || linkedResults.length || unlinkedResults.length || notesLinkingToItem.length
allLinkedItems.length || linkedResults.length || unlinkedResults.length || notesLinkingToActiveItem.length
? 'border-b border-border pb-2.5'
: 'pb-1',
)}
@@ -246,14 +247,14 @@ const LinkedItemsPanel = ({
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked</div>
<div className="my-1">
{linkedResults.map((item) => (
{linkedResults.map((link) => (
<LinkedItemsSectionItem
key={item.uuid}
item={item}
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={unlinkItemFromSelectedItem}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
@@ -268,14 +269,14 @@ const LinkedItemsPanel = ({
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Tags</div>
<div className="my-1">
{tags.map((item) => (
{tags.map((link) => (
<LinkedItemsSectionItem
key={item.uuid}
item={item}
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={unlinkItemFromSelectedItem}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
@@ -283,18 +284,39 @@ const LinkedItemsPanel = ({
</div>
</div>
)}
{!!files.length && (
{!!linkedFiles.length && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Files</div>
<div className="my-1">
{files.map((item) => (
{linkedFiles.map((link) => (
<LinkedItemsSectionItem
key={item.uuid}
item={item}
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={unlinkItemFromSelectedItem}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
))}
</div>
</div>
)}
{!!filesLinkingToActiveItem.length && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">
Files Linking To Current File
</div>
<div className="my-1">
{filesLinkingToActiveItem.map((link) => (
<LinkedItemsSectionItem
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
@@ -306,14 +328,14 @@ const LinkedItemsPanel = ({
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Notes</div>
<div className="my-1">
{notesLinkedToItem.map((item) => (
{notesLinkedToItem.map((link) => (
<LinkedItemsSectionItem
key={item.uuid}
item={item}
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={unlinkItemFromSelectedItem}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
@@ -321,20 +343,20 @@ const LinkedItemsPanel = ({
</div>
</div>
)}
{!!notesLinkingToItem.length && (
{!!notesLinkingToActiveItem.length && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">
Notes Linking To This Note
</div>
<div className="my-1">
{notesLinkingToItem.map((item) => (
{notesLinkingToActiveItem.map((link) => (
<LinkedItemsSectionItem
key={item.uuid}
item={item}
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={unlinkItemFromSelectedItem}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>

View File

@@ -99,7 +99,7 @@ export class FilesController extends AbstractViewController {
reloadAttachedFiles = () => {
const note = this.notesController.firstSelectedNote
if (note) {
this.attachedFiles = this.application.items.getSortedFilesForItem(note)
this.attachedFiles = this.application.items.getSortedFilesLinkingToItem(note)
}
}

View File

@@ -10,7 +10,9 @@ import {
IconType,
InternalEventBus,
ItemContent,
ItemsClientInterface,
naturalSort,
NoteViewController,
PrefKey,
SNNote,
SNTag,
@@ -25,11 +27,20 @@ import { SubscriptionController } from './Subscription/SubscriptionController'
export type LinkableItem = DecryptedItemInterface<ItemContent>
type RelationWithSelectedItem = ReturnType<ItemsClientInterface['relationshipTypeForItems']>
export type ItemLink<ItemType extends LinkableItem = LinkableItem> = {
id: string
item: ItemType
relationWithSelectedItem: RelationWithSelectedItem
}
export class LinkingController extends AbstractViewController {
tags: SNTag[] = []
files: FileItem[] = []
notesLinkedToItem: SNNote[] = []
notesLinkingToItem: SNNote[] = []
tags: ItemLink<SNTag>[] = []
linkedFiles: ItemLink<FileItem>[] = []
filesLinkingToActiveItem: ItemLink<FileItem>[] = []
notesLinkedToItem: ItemLink<SNNote>[] = []
notesLinkingToActiveItem: ItemLink<SNNote>[] = []
shouldLinkToParentFolders: boolean
isLinkingPanelOpen = false
private itemListController!: ItemListController
@@ -46,13 +57,15 @@ export class LinkingController extends AbstractViewController {
makeObservable(this, {
tags: observable,
files: observable,
linkedFiles: observable,
filesLinkingToActiveItem: observable,
notesLinkedToItem: observable,
notesLinkingToItem: observable,
notesLinkingToActiveItem: observable,
isLinkingPanelOpen: observable,
allLinkedItems: computed,
allItemLinks: computed,
isEntitledToNoteLinking: computed,
selectedItemTitle: computed,
setIsLinkingPanelOpen: action,
reloadLinkedFiles: action,
@@ -107,14 +120,22 @@ export class LinkingController extends AbstractViewController {
this.isLinkingPanelOpen = open
}
get allLinkedItems() {
return [...this.tags, ...this.files, ...this.notesLinkedToItem]
get allItemLinks() {
return [...this.tags, ...this.linkedFiles, ...this.notesLinkedToItem]
}
get activeItem() {
return this.itemListController.activeControllerItem
}
get selectedItemTitle() {
return this.selectionController.firstSelectedItem
? this.selectionController.firstSelectedItem.title
: this.activeItem
? this.activeItem.title
: ''
}
reloadAllLinks() {
this.reloadLinkedFiles()
this.reloadLinkedTags()
@@ -122,31 +143,67 @@ export class LinkingController extends AbstractViewController {
this.reloadNotesLinkingToItem()
}
createLinkFromItem = <ItemType extends LinkableItem = LinkableItem>(
item: ItemType,
relation?: RelationWithSelectedItem,
): ItemLink<ItemType> => {
const relationWithSelectedItem = relation ? relation : this.itemRelationshipWithSelectedItem(item)
return {
id: `${item.uuid}-${relationWithSelectedItem}`,
item,
relationWithSelectedItem,
}
}
reloadLinkedFiles() {
if (this.activeItem) {
const files = this.application.items.getSortedFilesForItem(this.activeItem)
this.files = files
if (!this.activeItem) {
return
}
const isActiveItemAFile = this.activeItem instanceof FileItem
const linkedFiles = this.application.items
.getSortedLinkedFilesForItem(this.activeItem)
.map((item) => this.createLinkFromItem(item, isActiveItemAFile ? 'direct' : 'indirect'))
const filesLinkingToActiveItem = this.application.items
.getSortedFilesLinkingToItem(this.activeItem)
.map((item) => this.createLinkFromItem(item, isActiveItemAFile ? 'indirect' : 'direct'))
if (isActiveItemAFile) {
this.linkedFiles = linkedFiles
this.filesLinkingToActiveItem = filesLinkingToActiveItem
} else {
this.linkedFiles = filesLinkingToActiveItem
this.filesLinkingToActiveItem = linkedFiles
}
}
reloadLinkedTags() {
if (this.activeItem) {
const tags = this.application.items.getSortedTagsForItem(this.activeItem)
const tags = this.application.items
.getSortedTagsForItem(this.activeItem)
.map((item) => this.createLinkFromItem(item))
this.tags = tags
}
}
reloadLinkedNotes() {
if (this.activeItem) {
const notes = this.application.items.getSortedLinkedNotesForItem(this.activeItem)
const notes = this.application.items
.getSortedLinkedNotesForItem(this.activeItem)
.map((item) => this.createLinkFromItem(item, 'direct'))
this.notesLinkedToItem = notes
}
}
reloadNotesLinkingToItem() {
if (this.activeItem) {
const notes = this.application.items.getSortedNotesLinkingToItem(this.activeItem)
this.notesLinkingToItem = notes
const notes = this.application.items
.getSortedNotesLinkingToItem(this.activeItem)
.map((item) => this.createLinkFromItem(item, 'indirect'))
this.notesLinkingToActiveItem = notes
}
}
@@ -206,44 +263,61 @@ export class LinkingController extends AbstractViewController {
return undefined
}
unlinkItemFromSelectedItem = async (itemToUnlink: LinkableItem) => {
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
if (!selectedItem) {
return
}
const selectedItemReferencesItemToUnlink =
this.application.items.relationshipTypeForItems(selectedItem, itemToUnlink) === 'direct'
const selectedItemReferencesItemToUnlink = itemToUnlink.relationWithSelectedItem === 'direct'
if (selectedItemReferencesItemToUnlink) {
await this.application.items.unlinkItem(selectedItem, itemToUnlink)
await this.application.items.unlinkItem(selectedItem, itemToUnlink.item)
} else {
await this.application.items.unlinkItem(itemToUnlink, selectedItem)
await this.application.items.unlinkItem(itemToUnlink.item, selectedItem)
}
void this.application.sync.sync()
this.reloadAllLinks()
}
linkItemToSelectedItem = async (itemToLink: LinkableItem) => {
const selectedItem = this.selectionController.firstSelectedItem
ensureActiveItemIsInserted = async () => {
const activeItemController = this.itemListController.getActiveItemController()
if (activeItemController instanceof NoteViewController && activeItemController.isTemplateNote) {
await activeItemController.insertTemplatedNote()
}
}
if (itemToLink instanceof SNTag) {
await this.addTagToActiveItem(itemToLink)
linkItemToSelectedItem = async (itemToLink: LinkableItem) => {
await this.ensureActiveItemIsInserted()
const activeItem = this.activeItem
if (activeItem && itemToLink instanceof SNTag) {
await this.addTagToItem(itemToLink, activeItem)
}
if (selectedItem instanceof SNNote) {
if (activeItem instanceof SNNote) {
if (itemToLink instanceof FileItem) {
await this.application.items.associateFileWithNote(itemToLink, selectedItem)
await this.application.items.associateFileWithNote(itemToLink, activeItem)
} else if (itemToLink instanceof SNNote && this.isEntitledToNoteLinking) {
await this.application.items.linkNoteToNote(selectedItem, itemToLink)
await this.application.items.linkNoteToNote(activeItem, itemToLink)
}
} else if (selectedItem instanceof FileItem) {
} else if (activeItem instanceof FileItem) {
if (itemToLink instanceof SNNote) {
await this.application.items.associateFileWithNote(selectedItem, itemToLink)
await this.application.items.associateFileWithNote(activeItem, itemToLink)
} else if (itemToLink instanceof FileItem) {
await this.application.items.linkFileToFile(itemToLink, selectedItem)
await this.application.items.linkFileToFile(activeItem, itemToLink)
}
}
@@ -252,21 +326,19 @@ export class LinkingController extends AbstractViewController {
}
createAndAddNewTag = async (title: string) => {
await this.ensureActiveItemIsInserted()
const activeItem = this.activeItem
const newTag = await this.application.mutator.findOrCreateTag(title)
await this.addTagToActiveItem(newTag)
if (activeItem) {
await this.addTagToItem(newTag, activeItem)
}
}
addTagToActiveItem = async (tag: SNTag) => {
const activeItem = this.itemListController.activeControllerItem
if (!activeItem) {
return
}
if (activeItem instanceof SNNote) {
await this.application.items.addTagToNote(activeItem, tag, this.shouldLinkToParentFolders)
} else if (activeItem instanceof FileItem) {
await this.application.items.addTagToFile(activeItem, tag, this.shouldLinkToParentFolders)
addTagToItem = async (tag: SNTag, item: FileItem | SNNote) => {
if (item instanceof SNNote) {
await this.application.items.addTagToNote(item, tag, this.shouldLinkToParentFolders)
} else if (item instanceof FileItem) {
await this.application.items.addTagToFile(item, tag, this.shouldLinkToParentFolders)
}
this.reloadLinkedTags()
@@ -293,19 +365,23 @@ export class LinkingController extends AbstractViewController {
'title',
)
const isAlreadyLinked = (item: LinkableItem) => {
const isAlreadyLinked = (item: DecryptedItemInterface<ItemContent>) => {
if (!this.activeItem) {
return false
}
const isItemReferencedByActiveItem = this.application.items
.itemsReferencingItem(item)
.some((linkedItem) => linkedItem.uuid === this.activeItem?.uuid)
const isActiveItemReferencedByItem = this.application.items
.itemsReferencingItem(this.activeItem)
.some((linkedItem) => linkedItem.uuid === item.uuid)
const isAlreadyLinkedToItem =
isItemReferencedByActiveItem || (item.content_type !== ContentType.Note && isActiveItemReferencedByItem)
return isAlreadyLinkedToItem
if (this.activeItem.content_type === item.content_type) {
return isItemReferencedByActiveItem
}
return isActiveItemReferencedByItem || isItemReferencedByActiveItem
}
const prioritizeTagResult = (
@@ -325,10 +401,14 @@ export class LinkingController extends AbstractViewController {
.slice(0, 20)
.filter((item) => !isAlreadyLinked(item))
.sort(prioritizeTagResult)
const linkedResults = searchResults.filter(isAlreadyLinked).slice(0, 20)
const isResultExistingTag = (result: LinkableItem) =>
const linkedResults = searchResults
.filter(isAlreadyLinked)
.slice(0, 20)
.map((item) => this.createLinkFromItem(item))
const isResultExistingTag = (result: DecryptedItemInterface<ItemContent>) =>
result.content_type === ContentType.Tag && result.title === searchQuery
const shouldShowCreateTag = !linkedResults.find(isResultExistingTag) && !unlinkedResults.find(isResultExistingTag)
const shouldShowCreateTag =
!linkedResults.find((link) => isResultExistingTag(link.item)) && !unlinkedResults.find(isResultExistingTag)
return {
unlinkedResults,