refactor: item linking (#1781)
This commit is contained in:
@@ -68,7 +68,7 @@ export const useFiles = ({ note }: Props) => {
|
|||||||
const filesService = application.getFilesService()
|
const filesService = application.getFilesService()
|
||||||
|
|
||||||
const reloadAttachedFiles = useCallback(() => {
|
const reloadAttachedFiles = useCallback(() => {
|
||||||
setAttachedFiles(application.items.getSortedFilesForItem(note).sort(filesService.sortByName))
|
setAttachedFiles(application.items.getSortedFilesLinkingToItem(note).sort(filesService.sortByName))
|
||||||
}, [application.items, filesService.sortByName, note])
|
}, [application.items, filesService.sortByName, note])
|
||||||
|
|
||||||
const reloadAllFiles = useCallback(() => {
|
const reloadAllFiles = useCallback(() => {
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
|
|||||||
setAttachedFilesLength(0)
|
setAttachedFilesLength(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setAttachedFilesLength(application.items.getSortedFilesForItem(note).length)
|
setAttachedFilesLength(application.items.getSortedFilesLinkingToItem(note).length)
|
||||||
}, [application, note])
|
}, [application, note])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -147,7 +147,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const removeFilesObserver = application.streamItems(ContentType.File, () => {
|
const removeFilesObserver = application.streamItems(ContentType.File, () => {
|
||||||
setAttachedFilesLength(application.items.getSortedFilesForItem(note).length)
|
setAttachedFilesLength(application.items.getSortedFilesLinkingToItem(note).length)
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
removeFilesObserver()
|
removeFilesObserver()
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ export interface ItemsClientInterface {
|
|||||||
* @returns Array containing tags associated with an item
|
* @returns Array containing tags associated with an item
|
||||||
*/
|
*/
|
||||||
getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): SNTag[]
|
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[]
|
getSortedLinkedNotesForItem(item: DecryptedItemInterface<ItemContent>): SNNote[]
|
||||||
getSortedNotesLinkingToItem(item: DecryptedItemInterface<ItemContent>): SNNote[]
|
getSortedNotesLinkingToItem(item: DecryptedItemInterface<ItemContent>): SNNote[]
|
||||||
|
|||||||
@@ -805,7 +805,7 @@ describe('itemManager', () => {
|
|||||||
|
|
||||||
await itemManager.associateFileWithNote(file, note)
|
await itemManager.associateFileWithNote(file, note)
|
||||||
|
|
||||||
const filesAssociatedWithNote = itemManager.getSortedFilesForItem(note)
|
const filesAssociatedWithNote = itemManager.getSortedFilesLinkingToItem(note)
|
||||||
|
|
||||||
expect(filesAssociatedWithNote).toHaveLength(1)
|
expect(filesAssociatedWithNote).toHaveLength(1)
|
||||||
expect(filesAssociatedWithNote[0].uuid).toBe(file.uuid)
|
expect(filesAssociatedWithNote[0].uuid).toBe(file.uuid)
|
||||||
@@ -873,20 +873,38 @@ describe('itemManager', () => {
|
|||||||
|
|
||||||
it('should get all linked files for item', async () => {
|
it('should get all linked files for item', async () => {
|
||||||
itemManager = createService()
|
itemManager = createService()
|
||||||
const note = createNote('note')
|
|
||||||
const file = createFile('A1')
|
const file = createFile('A1')
|
||||||
const file2 = createFile('B2')
|
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.linkFileToFile(file, file3)
|
||||||
await itemManager.associateFileWithNote(file, note)
|
await itemManager.linkFileToFile(file, file2)
|
||||||
|
|
||||||
const sortedFilesForItem = itemManager.getSortedFilesForItem(note)
|
const sortedFilesForItem = itemManager.getSortedLinkedFilesForItem(file)
|
||||||
|
|
||||||
expect(sortedFilesForItem).toHaveLength(2)
|
expect(sortedFilesForItem).toHaveLength(2)
|
||||||
expect(sortedFilesForItem[0].uuid).toEqual(file.uuid)
|
expect(sortedFilesForItem[0].uuid).toEqual(file2.uuid)
|
||||||
expect(sortedFilesForItem[1].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 () => {
|
it('should get all linked notes for item', async () => {
|
||||||
|
|||||||
@@ -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)) {
|
if (this.isTemplateItem(item)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -1200,11 +1212,8 @@ export class ItemManager
|
|||||||
const filesReferencingItem = this.itemsReferencingItem(item).filter(
|
const filesReferencingItem = this.itemsReferencingItem(item).filter(
|
||||||
(ref) => ref.content_type === ContentType.File,
|
(ref) => ref.content_type === ContentType.File,
|
||||||
) as Models.FileItem[]
|
) 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[] {
|
public getSortedLinkedNotesForItem(item: DecryptedItemInterface<ItemContent>): Models.SNNote[] {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
||||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||||
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
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) => {
|
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||||
notesController.setContextMenuOpen(false)
|
notesController.setContextMenuOpen(false)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as icons from '@standardnotes/icons'
|
|||||||
export const ICONS = {
|
export const ICONS = {
|
||||||
'account-circle': icons.AccountCircleIcon,
|
'account-circle': icons.AccountCircleIcon,
|
||||||
'arrow-left': icons.ArrowLeftIcon,
|
'arrow-left': icons.ArrowLeftIcon,
|
||||||
|
'arrow-right': icons.ArrowRightIcon,
|
||||||
'arrows-sort-down': icons.ArrowsSortDownIcon,
|
'arrows-sort-down': icons.ArrowsSortDownIcon,
|
||||||
'arrows-sort-up': icons.ArrowsSortUpIcon,
|
'arrows-sort-up': icons.ArrowsSortUpIcon,
|
||||||
'attachment-file': icons.AttachmentFileIcon,
|
'attachment-file': icons.AttachmentFileIcon,
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
import { ItemLink, LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
||||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { KeyboardEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react'
|
import { KeyboardEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||||
|
import { ContentType } from '@standardnotes/snjs'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: LinkableItem
|
link: ItemLink
|
||||||
getItemIcon: LinkingController['getLinkedItemIcon']
|
getItemIcon: LinkingController['getLinkedItemIcon']
|
||||||
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
|
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
|
||||||
activateItem: (item: LinkableItem) => Promise<void>
|
activateItem: (item: LinkableItem) => Promise<void>
|
||||||
unlinkItem: (item: LinkableItem) => void
|
unlinkItem: LinkingController['unlinkItemFromSelectedItem']
|
||||||
focusPreviousItem: () => void
|
focusPreviousItem: () => void
|
||||||
focusNextItem: () => void
|
focusNextItem: () => void
|
||||||
focusedId: string | undefined
|
focusedId: string | undefined
|
||||||
setFocusedId: (id: string) => void
|
setFocusedId: (id: string) => void
|
||||||
|
isBidirectional: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const LinkedItemBubble = ({
|
const LinkedItemBubble = ({
|
||||||
item,
|
link,
|
||||||
getItemIcon,
|
getItemIcon,
|
||||||
getTitleForLinkedTag,
|
getTitleForLinkedTag,
|
||||||
activateItem,
|
activateItem,
|
||||||
@@ -27,6 +29,7 @@ const LinkedItemBubble = ({
|
|||||||
focusNextItem,
|
focusNextItem,
|
||||||
focusedId,
|
focusedId,
|
||||||
setFocusedId,
|
setFocusedId,
|
||||||
|
isBidirectional,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const ref = useRef<HTMLButtonElement>(null)
|
const ref = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
@@ -36,8 +39,8 @@ const LinkedItemBubble = ({
|
|||||||
const [wasClicked, setWasClicked] = useState(false)
|
const [wasClicked, setWasClicked] = useState(false)
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
if (focusedId !== item.uuid) {
|
if (focusedId !== link.id) {
|
||||||
setFocusedId(item.uuid)
|
setFocusedId(link.id)
|
||||||
}
|
}
|
||||||
setShowUnlinkButton(true)
|
setShowUnlinkButton(true)
|
||||||
}
|
}
|
||||||
@@ -50,7 +53,7 @@ const LinkedItemBubble = ({
|
|||||||
const onClick: MouseEventHandler = (event) => {
|
const onClick: MouseEventHandler = (event) => {
|
||||||
if (wasClicked && event.target !== unlinkButtonRef.current) {
|
if (wasClicked && event.target !== unlinkButtonRef.current) {
|
||||||
setWasClicked(false)
|
setWasClicked(false)
|
||||||
void activateItem(item)
|
void activateItem(link.item)
|
||||||
} else {
|
} else {
|
||||||
setWasClicked(true)
|
setWasClicked(true)
|
||||||
}
|
}
|
||||||
@@ -58,14 +61,14 @@ const LinkedItemBubble = ({
|
|||||||
|
|
||||||
const onUnlinkClick: MouseEventHandler = (event) => {
|
const onUnlinkClick: MouseEventHandler = (event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
unlinkItem(item)
|
unlinkItem(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown: KeyboardEventHandler = (event) => {
|
const onKeyDown: KeyboardEventHandler = (event) => {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case KeyboardKey.Backspace: {
|
case KeyboardKey.Backspace: {
|
||||||
focusPreviousItem()
|
focusPreviousItem()
|
||||||
unlinkItem(item)
|
unlinkItem(link)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case KeyboardKey.Left:
|
case KeyboardKey.Left:
|
||||||
@@ -77,29 +80,34 @@ const LinkedItemBubble = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [icon, iconClassName] = getItemIcon(item)
|
const [icon, iconClassName] = getItemIcon(link.item)
|
||||||
const tagTitle = getTitleForLinkedTag(item)
|
const tagTitle = getTitleForLinkedTag(link.item)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item.uuid === focusedId) {
|
if (link.id === focusedId) {
|
||||||
ref.current?.focus()
|
ref.current?.focus()
|
||||||
}
|
}
|
||||||
}, [focusedId, item.uuid])
|
}, [focusedId, link.id])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
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}
|
onFocus={handleFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title={tagTitle ? tagTitle.longTitle : item.title}
|
title={tagTitle ? tagTitle.longTitle : link.item.title}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
<Icon type={icon} className={classNames('mr-1 flex-shrink-0', iconClassName)} size="small" />
|
<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>}
|
{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>
|
</span>
|
||||||
{showUnlinkButton && (
|
{showUnlinkButton && (
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
||||||
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
import { ItemLink, LinkableItem, LinkingController } from '@/Controllers/LinkingController'
|
||||||
import LinkedItemBubble from './LinkedItemBubble'
|
import LinkedItemBubble from './LinkedItemBubble'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||||
@@ -14,8 +14,9 @@ type Props = {
|
|||||||
const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
|
const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
const {
|
const {
|
||||||
allLinkedItems,
|
allItemLinks,
|
||||||
notesLinkingToItem,
|
notesLinkingToActiveItem,
|
||||||
|
filesLinkingToActiveItem,
|
||||||
unlinkItemFromSelectedItem: unlinkItem,
|
unlinkItemFromSelectedItem: unlinkItem,
|
||||||
getTitleForLinkedTag,
|
getTitleForLinkedTag,
|
||||||
getLinkedItemIcon: getItemIcon,
|
getLinkedItemIcon: getItemIcon,
|
||||||
@@ -23,7 +24,13 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
|
|||||||
} = linkingController
|
} = linkingController
|
||||||
|
|
||||||
const [focusedId, setFocusedId] = useState<string>()
|
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 focusPreviousItem = useCallback(() => {
|
||||||
const currentFocusedIndex = focusableIds.findIndex((id) => id === focusedId)
|
const currentFocusedIndex = focusableIds.findIndex((id) => id === focusedId)
|
||||||
@@ -53,27 +60,38 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
|
|||||||
[activateItem, toggleAppPane],
|
[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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'hidden min-w-80 max-w-full flex-wrap items-center gap-2 bg-transparent md:-mr-2 md:flex',
|
'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) => (
|
{allItemLinks
|
||||||
<LinkedItemBubble
|
.concat(notesLinkingToActiveItem)
|
||||||
item={item}
|
.concat(filesLinkingToActiveItem)
|
||||||
key={item.uuid}
|
.map((link) => (
|
||||||
getItemIcon={getItemIcon}
|
<LinkedItemBubble
|
||||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
link={link}
|
||||||
activateItem={activateItemAndTogglePane}
|
key={link.id}
|
||||||
unlinkItem={unlinkItem}
|
getItemIcon={getItemIcon}
|
||||||
focusPreviousItem={focusPreviousItem}
|
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||||
focusNextItem={focusNextItem}
|
activateItem={activateItemAndTogglePane}
|
||||||
focusedId={focusedId}
|
unlinkItem={unlinkItem}
|
||||||
setFocusedId={setFocusedId}
|
focusPreviousItem={focusPreviousItem}
|
||||||
/>
|
focusNextItem={focusNextItem}
|
||||||
))}
|
focusedId={focusedId}
|
||||||
|
setFocusedId={setFocusedId}
|
||||||
|
isBidirectional={isItemBidirectionallyLinked(link)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<ItemLinkAutocompleteInput
|
<ItemLinkAutocompleteInput
|
||||||
focusedId={focusedId}
|
focusedId={focusedId}
|
||||||
linkingController={linkingController}
|
linkingController={linkingController}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const LinkedItemsSectionItem = ({
|
|||||||
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
|
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
|
||||||
item: LinkableItem
|
item: LinkableItem
|
||||||
searchQuery?: string
|
searchQuery?: string
|
||||||
unlinkItem: LinkingController['unlinkItemFromSelectedItem']
|
unlinkItem: () => void
|
||||||
handleFileAction: FilesController['handleFileAction']
|
handleFileAction: FilesController['handleFileAction']
|
||||||
}) => {
|
}) => {
|
||||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
@@ -85,7 +85,7 @@ const LinkedItemsSectionItem = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<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)}
|
onClick={() => activateItem(item)}
|
||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -118,7 +118,7 @@ const LinkedItemsSectionItem = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
type={MenuItemType.IconButton}
|
type={MenuItemType.IconButton}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
unlinkItem(item)
|
unlinkItem()
|
||||||
toggleMenu()
|
toggleMenu()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -166,10 +166,11 @@ const LinkedItemsPanel = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
files,
|
linkedFiles,
|
||||||
|
filesLinkingToActiveItem,
|
||||||
notesLinkedToItem,
|
notesLinkedToItem,
|
||||||
notesLinkingToItem,
|
notesLinkingToActiveItem,
|
||||||
allLinkedItems,
|
allItemLinks: allLinkedItems,
|
||||||
getTitleForLinkedTag,
|
getTitleForLinkedTag,
|
||||||
getLinkedItemIcon,
|
getLinkedItemIcon,
|
||||||
getSearchResults,
|
getSearchResults,
|
||||||
@@ -196,7 +197,7 @@ const LinkedItemsPanel = ({
|
|||||||
<form
|
<form
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'sticky top-0 z-10 bg-default px-2.5 pt-2.5',
|
'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'
|
? 'border-b border-border pb-2.5'
|
||||||
: 'pb-1',
|
: 'pb-1',
|
||||||
)}
|
)}
|
||||||
@@ -246,14 +247,14 @@ const LinkedItemsPanel = ({
|
|||||||
<div>
|
<div>
|
||||||
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked</div>
|
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked</div>
|
||||||
<div className="my-1">
|
<div className="my-1">
|
||||||
{linkedResults.map((item) => (
|
{linkedResults.map((link) => (
|
||||||
<LinkedItemsSectionItem
|
<LinkedItemsSectionItem
|
||||||
key={item.uuid}
|
key={link.id}
|
||||||
item={item}
|
item={link.item}
|
||||||
getItemIcon={getLinkedItemIcon}
|
getItemIcon={getLinkedItemIcon}
|
||||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={unlinkItemFromSelectedItem}
|
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -268,14 +269,14 @@ const LinkedItemsPanel = ({
|
|||||||
<div>
|
<div>
|
||||||
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Tags</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">
|
<div className="my-1">
|
||||||
{tags.map((item) => (
|
{tags.map((link) => (
|
||||||
<LinkedItemsSectionItem
|
<LinkedItemsSectionItem
|
||||||
key={item.uuid}
|
key={link.id}
|
||||||
item={item}
|
item={link.item}
|
||||||
getItemIcon={getLinkedItemIcon}
|
getItemIcon={getLinkedItemIcon}
|
||||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={unlinkItemFromSelectedItem}
|
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -283,18 +284,39 @@ const LinkedItemsPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!files.length && (
|
{!!linkedFiles.length && (
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Files</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">
|
<div className="my-1">
|
||||||
{files.map((item) => (
|
{linkedFiles.map((link) => (
|
||||||
<LinkedItemsSectionItem
|
<LinkedItemsSectionItem
|
||||||
key={item.uuid}
|
key={link.id}
|
||||||
item={item}
|
item={link.item}
|
||||||
getItemIcon={getLinkedItemIcon}
|
getItemIcon={getLinkedItemIcon}
|
||||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||||
searchQuery={searchQuery}
|
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}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -306,14 +328,14 @@ const LinkedItemsPanel = ({
|
|||||||
<div>
|
<div>
|
||||||
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Notes</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">
|
<div className="my-1">
|
||||||
{notesLinkedToItem.map((item) => (
|
{notesLinkedToItem.map((link) => (
|
||||||
<LinkedItemsSectionItem
|
<LinkedItemsSectionItem
|
||||||
key={item.uuid}
|
key={link.id}
|
||||||
item={item}
|
item={link.item}
|
||||||
getItemIcon={getLinkedItemIcon}
|
getItemIcon={getLinkedItemIcon}
|
||||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={unlinkItemFromSelectedItem}
|
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -321,20 +343,20 @@ const LinkedItemsPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!notesLinkingToItem.length && (
|
{!!notesLinkingToActiveItem.length && (
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">
|
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">
|
||||||
Notes Linking To This Note
|
Notes Linking To This Note
|
||||||
</div>
|
</div>
|
||||||
<div className="my-1">
|
<div className="my-1">
|
||||||
{notesLinkingToItem.map((item) => (
|
{notesLinkingToActiveItem.map((link) => (
|
||||||
<LinkedItemsSectionItem
|
<LinkedItemsSectionItem
|
||||||
key={item.uuid}
|
key={link.id}
|
||||||
item={item}
|
item={link.item}
|
||||||
getItemIcon={getLinkedItemIcon}
|
getItemIcon={getLinkedItemIcon}
|
||||||
getTitleForLinkedTag={getTitleForLinkedTag}
|
getTitleForLinkedTag={getTitleForLinkedTag}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={unlinkItemFromSelectedItem}
|
unlinkItem={() => unlinkItemFromSelectedItem(link)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export class FilesController extends AbstractViewController {
|
|||||||
reloadAttachedFiles = () => {
|
reloadAttachedFiles = () => {
|
||||||
const note = this.notesController.firstSelectedNote
|
const note = this.notesController.firstSelectedNote
|
||||||
if (note) {
|
if (note) {
|
||||||
this.attachedFiles = this.application.items.getSortedFilesForItem(note)
|
this.attachedFiles = this.application.items.getSortedFilesLinkingToItem(note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
IconType,
|
IconType,
|
||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
ItemContent,
|
ItemContent,
|
||||||
|
ItemsClientInterface,
|
||||||
naturalSort,
|
naturalSort,
|
||||||
|
NoteViewController,
|
||||||
PrefKey,
|
PrefKey,
|
||||||
SNNote,
|
SNNote,
|
||||||
SNTag,
|
SNTag,
|
||||||
@@ -25,11 +27,20 @@ import { SubscriptionController } from './Subscription/SubscriptionController'
|
|||||||
|
|
||||||
export type LinkableItem = DecryptedItemInterface<ItemContent>
|
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 {
|
export class LinkingController extends AbstractViewController {
|
||||||
tags: SNTag[] = []
|
tags: ItemLink<SNTag>[] = []
|
||||||
files: FileItem[] = []
|
linkedFiles: ItemLink<FileItem>[] = []
|
||||||
notesLinkedToItem: SNNote[] = []
|
filesLinkingToActiveItem: ItemLink<FileItem>[] = []
|
||||||
notesLinkingToItem: SNNote[] = []
|
notesLinkedToItem: ItemLink<SNNote>[] = []
|
||||||
|
notesLinkingToActiveItem: ItemLink<SNNote>[] = []
|
||||||
shouldLinkToParentFolders: boolean
|
shouldLinkToParentFolders: boolean
|
||||||
isLinkingPanelOpen = false
|
isLinkingPanelOpen = false
|
||||||
private itemListController!: ItemListController
|
private itemListController!: ItemListController
|
||||||
@@ -46,13 +57,15 @@ export class LinkingController extends AbstractViewController {
|
|||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
tags: observable,
|
tags: observable,
|
||||||
files: observable,
|
linkedFiles: observable,
|
||||||
|
filesLinkingToActiveItem: observable,
|
||||||
notesLinkedToItem: observable,
|
notesLinkedToItem: observable,
|
||||||
notesLinkingToItem: observable,
|
notesLinkingToActiveItem: observable,
|
||||||
isLinkingPanelOpen: observable,
|
isLinkingPanelOpen: observable,
|
||||||
|
|
||||||
allLinkedItems: computed,
|
allItemLinks: computed,
|
||||||
isEntitledToNoteLinking: computed,
|
isEntitledToNoteLinking: computed,
|
||||||
|
selectedItemTitle: computed,
|
||||||
|
|
||||||
setIsLinkingPanelOpen: action,
|
setIsLinkingPanelOpen: action,
|
||||||
reloadLinkedFiles: action,
|
reloadLinkedFiles: action,
|
||||||
@@ -107,14 +120,22 @@ export class LinkingController extends AbstractViewController {
|
|||||||
this.isLinkingPanelOpen = open
|
this.isLinkingPanelOpen = open
|
||||||
}
|
}
|
||||||
|
|
||||||
get allLinkedItems() {
|
get allItemLinks() {
|
||||||
return [...this.tags, ...this.files, ...this.notesLinkedToItem]
|
return [...this.tags, ...this.linkedFiles, ...this.notesLinkedToItem]
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeItem() {
|
get activeItem() {
|
||||||
return this.itemListController.activeControllerItem
|
return this.itemListController.activeControllerItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectedItemTitle() {
|
||||||
|
return this.selectionController.firstSelectedItem
|
||||||
|
? this.selectionController.firstSelectedItem.title
|
||||||
|
: this.activeItem
|
||||||
|
? this.activeItem.title
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
|
||||||
reloadAllLinks() {
|
reloadAllLinks() {
|
||||||
this.reloadLinkedFiles()
|
this.reloadLinkedFiles()
|
||||||
this.reloadLinkedTags()
|
this.reloadLinkedTags()
|
||||||
@@ -122,31 +143,67 @@ export class LinkingController extends AbstractViewController {
|
|||||||
this.reloadNotesLinkingToItem()
|
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() {
|
reloadLinkedFiles() {
|
||||||
if (this.activeItem) {
|
if (!this.activeItem) {
|
||||||
const files = this.application.items.getSortedFilesForItem(this.activeItem)
|
return
|
||||||
this.files = files
|
}
|
||||||
|
|
||||||
|
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() {
|
reloadLinkedTags() {
|
||||||
if (this.activeItem) {
|
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
|
this.tags = tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadLinkedNotes() {
|
reloadLinkedNotes() {
|
||||||
if (this.activeItem) {
|
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
|
this.notesLinkedToItem = notes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadNotesLinkingToItem() {
|
reloadNotesLinkingToItem() {
|
||||||
if (this.activeItem) {
|
if (this.activeItem) {
|
||||||
const notes = this.application.items.getSortedNotesLinkingToItem(this.activeItem)
|
const notes = this.application.items
|
||||||
this.notesLinkingToItem = notes
|
.getSortedNotesLinkingToItem(this.activeItem)
|
||||||
|
.map((item) => this.createLinkFromItem(item, 'indirect'))
|
||||||
|
this.notesLinkingToActiveItem = notes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,44 +263,61 @@ export class LinkingController extends AbstractViewController {
|
|||||||
return undefined
|
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
|
const selectedItem = this.selectionController.firstSelectedItem
|
||||||
|
|
||||||
if (!selectedItem) {
|
if (!selectedItem) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedItemReferencesItemToUnlink =
|
const selectedItemReferencesItemToUnlink = itemToUnlink.relationWithSelectedItem === 'direct'
|
||||||
this.application.items.relationshipTypeForItems(selectedItem, itemToUnlink) === 'direct'
|
|
||||||
|
|
||||||
if (selectedItemReferencesItemToUnlink) {
|
if (selectedItemReferencesItemToUnlink) {
|
||||||
await this.application.items.unlinkItem(selectedItem, itemToUnlink)
|
await this.application.items.unlinkItem(selectedItem, itemToUnlink.item)
|
||||||
} else {
|
} else {
|
||||||
await this.application.items.unlinkItem(itemToUnlink, selectedItem)
|
await this.application.items.unlinkItem(itemToUnlink.item, selectedItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.application.sync.sync()
|
void this.application.sync.sync()
|
||||||
this.reloadAllLinks()
|
this.reloadAllLinks()
|
||||||
}
|
}
|
||||||
|
|
||||||
linkItemToSelectedItem = async (itemToLink: LinkableItem) => {
|
ensureActiveItemIsInserted = async () => {
|
||||||
const selectedItem = this.selectionController.firstSelectedItem
|
const activeItemController = this.itemListController.getActiveItemController()
|
||||||
|
if (activeItemController instanceof NoteViewController && activeItemController.isTemplateNote) {
|
||||||
|
await activeItemController.insertTemplatedNote()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (itemToLink instanceof SNTag) {
|
linkItemToSelectedItem = async (itemToLink: LinkableItem) => {
|
||||||
await this.addTagToActiveItem(itemToLink)
|
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) {
|
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) {
|
} 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) {
|
if (itemToLink instanceof SNNote) {
|
||||||
await this.application.items.associateFileWithNote(selectedItem, itemToLink)
|
await this.application.items.associateFileWithNote(activeItem, itemToLink)
|
||||||
} else if (itemToLink instanceof FileItem) {
|
} 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) => {
|
createAndAddNewTag = async (title: string) => {
|
||||||
|
await this.ensureActiveItemIsInserted()
|
||||||
|
const activeItem = this.activeItem
|
||||||
const newTag = await this.application.mutator.findOrCreateTag(title)
|
const newTag = await this.application.mutator.findOrCreateTag(title)
|
||||||
await this.addTagToActiveItem(newTag)
|
if (activeItem) {
|
||||||
|
await this.addTagToItem(newTag, activeItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addTagToActiveItem = async (tag: SNTag) => {
|
addTagToItem = async (tag: SNTag, item: FileItem | SNNote) => {
|
||||||
const activeItem = this.itemListController.activeControllerItem
|
if (item instanceof SNNote) {
|
||||||
|
await this.application.items.addTagToNote(item, tag, this.shouldLinkToParentFolders)
|
||||||
if (!activeItem) {
|
} else if (item instanceof FileItem) {
|
||||||
return
|
await this.application.items.addTagToFile(item, tag, this.shouldLinkToParentFolders)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reloadLinkedTags()
|
this.reloadLinkedTags()
|
||||||
@@ -293,19 +365,23 @@ export class LinkingController extends AbstractViewController {
|
|||||||
'title',
|
'title',
|
||||||
)
|
)
|
||||||
|
|
||||||
const isAlreadyLinked = (item: LinkableItem) => {
|
const isAlreadyLinked = (item: DecryptedItemInterface<ItemContent>) => {
|
||||||
if (!this.activeItem) {
|
if (!this.activeItem) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const isItemReferencedByActiveItem = this.application.items
|
const isItemReferencedByActiveItem = this.application.items
|
||||||
.itemsReferencingItem(item)
|
.itemsReferencingItem(item)
|
||||||
.some((linkedItem) => linkedItem.uuid === this.activeItem?.uuid)
|
.some((linkedItem) => linkedItem.uuid === this.activeItem?.uuid)
|
||||||
const isActiveItemReferencedByItem = this.application.items
|
const isActiveItemReferencedByItem = this.application.items
|
||||||
.itemsReferencingItem(this.activeItem)
|
.itemsReferencingItem(this.activeItem)
|
||||||
.some((linkedItem) => linkedItem.uuid === item.uuid)
|
.some((linkedItem) => linkedItem.uuid === item.uuid)
|
||||||
const isAlreadyLinkedToItem =
|
|
||||||
isItemReferencedByActiveItem || (item.content_type !== ContentType.Note && isActiveItemReferencedByItem)
|
if (this.activeItem.content_type === item.content_type) {
|
||||||
return isAlreadyLinkedToItem
|
return isItemReferencedByActiveItem
|
||||||
|
}
|
||||||
|
|
||||||
|
return isActiveItemReferencedByItem || isItemReferencedByActiveItem
|
||||||
}
|
}
|
||||||
|
|
||||||
const prioritizeTagResult = (
|
const prioritizeTagResult = (
|
||||||
@@ -325,10 +401,14 @@ export class LinkingController extends AbstractViewController {
|
|||||||
.slice(0, 20)
|
.slice(0, 20)
|
||||||
.filter((item) => !isAlreadyLinked(item))
|
.filter((item) => !isAlreadyLinked(item))
|
||||||
.sort(prioritizeTagResult)
|
.sort(prioritizeTagResult)
|
||||||
const linkedResults = searchResults.filter(isAlreadyLinked).slice(0, 20)
|
const linkedResults = searchResults
|
||||||
const isResultExistingTag = (result: LinkableItem) =>
|
.filter(isAlreadyLinked)
|
||||||
|
.slice(0, 20)
|
||||||
|
.map((item) => this.createLinkFromItem(item))
|
||||||
|
const isResultExistingTag = (result: DecryptedItemInterface<ItemContent>) =>
|
||||||
result.content_type === ContentType.Tag && result.title === searchQuery
|
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 {
|
return {
|
||||||
unlinkedResults,
|
unlinkedResults,
|
||||||
|
|||||||
Reference in New Issue
Block a user