Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b591a14
refactor: item linking
amanharwara Oct 12, 2022
7c8abc3
refactor: linked search results
amanharwara Oct 12, 2022
10cbfe0
fix: error on new note
amanharwara Oct 12, 2022
0d220e7
Merge branch 'main' into refactor/item-linking
amanharwara Oct 12, 2022
1940196
fix: css variable fallback
amanharwara Oct 12, 2022
7f8c046
fix: linking for template notes
amanharwara Oct 12, 2022
c7cf78a
Merge branch 'main' into refactor/item-linking
amanharwara Oct 12, 2022
b753334
fix: ensure template note is inserted before linking
amanharwara Oct 12, 2022
e6f9756
Merge branch 'refactor/item-linking' of github.com:standardnotes/app …
amanharwara Oct 12, 2022
5bda5be
refactor: separate getter & insertion
amanharwara Oct 12, 2022
6fabcb8
fix: show link status if bidirectionally linked
amanharwara Oct 12, 2022
2c24b5e
feat: status
amanharwara Oct 12, 2022
5d5c9e6
feat: only show linked by text
amanharwara Oct 12, 2022
6d3f1a0
fix: file linking direction
amanharwara Oct 12, 2022
20743ad
feat: allow file bidirectional linking
amanharwara Oct 12, 2022
7c772e9
fix: somethings related to file bidirectional linking
amanharwara Oct 12, 2022
453813c
fix: test
amanharwara Oct 12, 2022
ca0a623
fix: file<->note & file<->file linking
amanharwara Oct 12, 2022
d594a3a
fix: keyboard nav
amanharwara Oct 12, 2022
4eff232
feat: test
amanharwara Oct 12, 2022
806955d
fix: lint error
amanharwara Oct 12, 2022
af6d513
Merge branch 'main' into refactor/item-linking
amanharwara Oct 12, 2022
f0e4686
fix: lint error
amanharwara Oct 12, 2022
68e7880
Merge branch 'refactor/item-linking' of github.com:standardnotes/app …
amanharwara Oct 12, 2022
9ad5446
Merge branch 'main' into refactor/item-linking
amanharwara Oct 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/mobile/src/Hooks/useFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
4 changes: 2 additions & 2 deletions packages/mobile/src/Screens/SideMenu/NoteSideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,15 @@ 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(() => {
if (!note) {
return
}
const removeFilesObserver = application.streamItems(ContentType.File, () => {
setAttachedFilesLength(application.items.getSortedFilesForItem(note).length)
setAttachedFilesLength(application.items.getSortedFilesLinkingToItem(note).length)
})
return () => {
removeFilesObserver()
Expand Down
4 changes: 3 additions & 1 deletion packages/services/src/Domain/Item/ItemsClientInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
34 changes: 26 additions & 8 deletions packages/snjs/lib/Services/Items/ItemManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.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.getSortedFilesForItem(note)
const sortedFilesForItem = itemManager.getSortedFilesLinkingToItem(baseFile)

expect(sortedFilesForItem).toHaveLength(2)
expect(sortedFilesForItem[0].uuid).toEqual(file.uuid)
expect(sortedFilesForItem[1].uuid).toEqual(file2.uuid)
expect(sortedFilesForItem[0].uuid).toEqual(fileToLink1.uuid)
expect(sortedFilesForItem[1].uuid).toEqual(fileToLink2.uuid)
})

it('should get all linked notes for item', async () => {
Expand Down
17 changes: 13 additions & 4 deletions packages/snjs/lib/Services/Items/ItemManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,19 +1192,28 @@ export class ItemManager
)
}

public getSortedFilesForItem(item: DecryptedItemInterface<ItemContent>): Models.FileItem[] {
public getSortedLinkedFilesForItem(item: DecryptedItemInterface<ItemContent>): Models.FileItem[] {
if (this.isTemplateItem(item)) {
return []
}

const filesReferencingItem = this.itemsReferencingItem(item).filter(
const filesReferencedByItem = this.referencesForItem(item).filter(
(ref) => ref.content_type === ContentType.File,
) as Models.FileItem[]
const filesReferencedByItem = this.referencesForItem(item).filter(

return naturalSort(filesReferencedByItem, 'title')
}

public getSortedFilesLinkingToItem(item: DecryptedItemInterface<ItemContent>): 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.concat(filesReferencedByItem), 'title')
return naturalSort(filesReferencingItem, 'title')
}

public getSortedLinkedNotesForItem(item: DecryptedItemInterface<ItemContent>): Models.SNNote[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/javascripts/Components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -27,6 +29,7 @@ const LinkedItemBubble = ({
focusNextItem,
focusedId,
setFocusedId,
isBidirectional,
}: Props) => {
const ref = useRef<HTMLButtonElement>(null)

Expand All @@ -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)
}
Expand All @@ -50,22 +53,22 @@ const LinkedItemBubble = ({
const onClick: MouseEventHandler = (event) => {
if (wasClicked && event.target !== unlinkButtonRef.current) {
setWasClicked(false)
void activateItem(item)
void activateItem(link.item)
} else {
setWasClicked(true)
}
}

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:
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -14,16 +14,23 @@ type Props = {
const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
const { toggleAppPane } = useResponsiveAppPane()
const {
allLinkedItems,
notesLinkingToItem,
allItemLinks,
notesLinkingToActiveItem,
filesLinkingToActiveItem,
unlinkItemFromSelectedItem: unlinkItem,
getTitleForLinkedTag,
getLinkedItemIcon: getItemIcon,
activateItem,
} = 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)
Expand Down Expand Up @@ -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}
Expand Down
Loading