refactor: de-couple linking controller from active item (#2108)

This commit is contained in:
Aman Harwara
2022-12-19 20:08:57 +05:30
committed by GitHub
parent 2b84c242f7
commit 31bb03943d
9 changed files with 211 additions and 200 deletions

View File

@@ -27,6 +27,7 @@ import ApplicationProvider from '../ApplicationProvider'
import CommandProvider from '../CommandProvider' import CommandProvider from '../CommandProvider'
import PanesSystemComponent from '../Panes/PanesSystemComponent' import PanesSystemComponent from '../Panes/PanesSystemComponent'
import DotOrgNotice from './DotOrgNotice' import DotOrgNotice from './DotOrgNotice'
import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -177,62 +178,61 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
application={application} application={application}
featuresController={viewControllerManager.featuresController} featuresController={viewControllerManager.featuresController}
> >
<div className={platformString + ' main-ui-view sn-component h-full'}> <LinkingControllerProvider controller={viewControllerManager.linkingController}>
<FileDragNDropProvider <div className={platformString + ' main-ui-view sn-component h-full'}>
application={application} <FileDragNDropProvider
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
>
<PanesSystemComponent />
</FileDragNDropProvider>
<>
<Footer application={application} applicationGroup={mainApplicationGroup} />
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
<RevisionHistoryModal
application={application} application={application}
historyModalController={viewControllerManager.historyModalController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
subscriptionController={viewControllerManager.subscriptionController}
/>
</>
{renderChallenges()}
<>
<NotesContextMenu
application={application}
navigationController={viewControllerManager.navigationController}
notesController={viewControllerManager.notesController}
linkingController={viewControllerManager.linkingController}
historyModalController={viewControllerManager.historyModalController}
/>
<TagContextMenuWrapper
navigationController={viewControllerManager.navigationController}
featuresController={viewControllerManager.featuresController} featuresController={viewControllerManager.featuresController}
/>
<FileContextMenuWrapper
filesController={viewControllerManager.filesController} filesController={viewControllerManager.filesController}
selectionController={viewControllerManager.selectionController} >
/> <PanesSystemComponent />
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} /> </FileDragNDropProvider>
<ConfirmSignoutContainer <>
applicationGroup={mainApplicationGroup} <Footer application={application} applicationGroup={mainApplicationGroup} />
viewControllerManager={viewControllerManager} <SessionsModal application={application} viewControllerManager={viewControllerManager} />
application={application} <PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
/> <RevisionHistoryModal
<ToastContainer /> application={application}
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} /> historyModalController={viewControllerManager.historyModalController}
<PermissionsModalWrapper application={application} /> notesController={viewControllerManager.notesController}
<ConfirmDeleteAccountContainer selectionController={viewControllerManager.selectionController}
application={application} subscriptionController={viewControllerManager.subscriptionController}
viewControllerManager={viewControllerManager} />
/> </>
</> {renderChallenges()}
{application.routeService.isDotOrg && <DotOrgNotice />} <>
</div> <NotesContextMenu
application={application}
navigationController={viewControllerManager.navigationController}
notesController={viewControllerManager.notesController}
linkingController={viewControllerManager.linkingController}
historyModalController={viewControllerManager.historyModalController}
/>
<TagContextMenuWrapper
navigationController={viewControllerManager.navigationController}
featuresController={viewControllerManager.featuresController}
/>
<FileContextMenuWrapper
filesController={viewControllerManager.filesController}
selectionController={viewControllerManager.selectionController}
/>
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
<ConfirmSignoutContainer
applicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
application={application}
/>
<ToastContainer />
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
<PermissionsModalWrapper application={application} />
<ConfirmDeleteAccountContainer
application={application}
viewControllerManager={viewControllerManager}
/>
</>
{application.routeService.isDotOrg && <DotOrgNotice />}
</div>
</LinkingControllerProvider>
</PremiumModalProvider> </PremiumModalProvider>
</ResponsivePaneProvider> </ResponsivePaneProvider>
</AndroidBackHandlerProvider> </AndroidBackHandlerProvider>

View File

@@ -20,6 +20,7 @@ import { ElementIds } from '@/Constants/ElementIDs'
import Menu from '../Menu/Menu' import Menu from '../Menu/Menu'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults' import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import { useApplication } from '../ApplicationProvider' import { useApplication } from '../ApplicationProvider'
import { DecryptedItem } from '@standardnotes/snjs'
type Props = { type Props = {
linkingController: LinkingController linkingController: LinkingController
@@ -27,6 +28,7 @@ type Props = {
focusedId: string | undefined focusedId: string | undefined
setFocusedId: (id: string) => void setFocusedId: (id: string) => void
hoverLabel?: string hoverLabel?: string
item: DecryptedItem
} }
const ItemLinkAutocompleteInput = ({ const ItemLinkAutocompleteInput = ({
@@ -35,12 +37,16 @@ const ItemLinkAutocompleteInput = ({
focusedId, focusedId,
setFocusedId, setFocusedId,
hoverLabel, hoverLabel,
item,
}: Props) => { }: Props) => {
const application = useApplication() const application = useApplication()
const { tags, linkItemToSelectedItem, createAndAddNewTag, isEntitledToNoteLinking, activeItem } = linkingController
const { getLinkedTagsForItem, linkItems, createAndAddNewTag, isEntitledToNoteLinking } = linkingController
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, activeItem) const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, item)
const [dropdownVisible, setDropdownVisible] = useState(false) const [dropdownVisible, setDropdownVisible] = useState(false)
const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>('auto') const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>('auto')
@@ -122,7 +128,7 @@ const ItemLinkAutocompleteInput = ({
<input <input
ref={inputRef} ref={inputRef}
className={classNames( className={classNames(
`${tags.length > 0 ? 'w-80' : 'mr-10 w-70'}`, `${tagsLinkedToItem.length > 0 ? 'w-80' : 'mr-10 w-70'}`,
'bg-transparent text-sm text-text focus:border-b-2 focus:border-solid focus:border-info lg:text-xs', 'bg-transparent text-sm text-text focus:border-b-2 focus:border-solid focus:border-info lg:text-xs',
'no-border h-7 focus:shadow-none focus:outline-none', 'no-border h-7 focus:shadow-none focus:outline-none',
)} )}
@@ -141,7 +147,7 @@ const ItemLinkAutocompleteInput = ({
{areSearchResultsVisible && ( {areSearchResultsVisible && (
<DisclosurePanel <DisclosurePanel
className={classNames( className={classNames(
tags.length > 0 ? 'w-80' : 'mr-10 w-70', tagsLinkedToItem.length > 0 ? 'w-80' : 'mr-10 w-70',
'absolute z-dropdown-menu flex flex-col overflow-y-auto rounded bg-default py-2 shadow-main', 'absolute z-dropdown-menu flex flex-col overflow-y-auto rounded bg-default py-2 shadow-main',
)} )}
style={{ style={{
@@ -159,7 +165,8 @@ const ItemLinkAutocompleteInput = ({
> >
<LinkedItemSearchResults <LinkedItemSearchResults
createAndAddNewTag={createAndAddNewTag} createAndAddNewTag={createAndAddNewTag}
linkItemToSelectedItem={linkItemToSelectedItem} linkItems={linkItems}
item={item}
results={unlinkedItems} results={unlinkedItems}
searchQuery={searchQuery} searchQuery={searchQuery}
shouldShowCreateTag={shouldShowCreateTag} shouldShowCreateTag={shouldShowCreateTag}

View File

@@ -11,23 +11,30 @@ import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { ItemLink } from '@/Utils/Items/Search/ItemLink' import { ItemLink } from '@/Utils/Items/Search/ItemLink'
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services' import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
import { useCommandService } from '../CommandProvider' import { useCommandService } from '../CommandProvider'
import { useApplication } from '../ApplicationProvider'
import { useItemLinks } from '@/Hooks/useItemLinks'
type Props = { type Props = {
linkingController: LinkingController linkingController: LinkingController
} }
const LinkedItemBubblesContainer = ({ linkingController }: Props) => { const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
const application = useApplication()
const activeItem = application.itemControllerGroup.activeItemViewController?.item
const { toggleAppPane } = useResponsiveAppPane() const { toggleAppPane } = useResponsiveAppPane()
const commandService = useCommandService() const commandService = useCommandService()
const { const { unlinkItemFromSelectedItem: unlinkItem, activateItem } = linkingController
allItemLinks,
notesLinkingToActiveItem, const { notesLinkedToItem, filesLinkedToItem, tagsLinkedToItem, notesLinkingToItem, filesLinkingToItem } =
filesLinkingToActiveItem, useItemLinks(activeItem)
unlinkItemFromSelectedItem: unlinkItem,
activateItem, const allItemsLinkedToItem: ItemLink[] = useMemo(
} = linkingController () => new Array<ItemLink>().concat(notesLinkedToItem, filesLinkedToItem, tagsLinkedToItem),
[filesLinkedToItem, notesLinkedToItem, tagsLinkedToItem],
)
useEffect(() => { useEffect(() => {
return commandService.addCommandHandler({ return commandService.addCommandHandler({
@@ -47,11 +54,11 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
) )
const [focusedId, setFocusedId] = useState<string>() const [focusedId, setFocusedId] = useState<string>()
const focusableIds = allItemLinks const focusableIds = allItemsLinkedToItem
.map((link) => link.id) .map((link) => link.id)
.concat( .concat(
notesLinkingToActiveItem.map((link) => link.id), notesLinkingToItem.map((link) => link.id),
filesLinkingToActiveItem.map((link) => link.id), filesLinkingToItem.map((link) => link.id),
[ElementIds.ItemLinkAutocompleteInput], [ElementIds.ItemLinkAutocompleteInput],
) )
@@ -84,9 +91,9 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
) )
const isItemBidirectionallyLinked = (link: ItemLink) => { const isItemBidirectionallyLinked = (link: ItemLink) => {
const existsInAllItemLinks = !!allItemLinks.find((item) => link.item.uuid === item.item.uuid) const existsInAllItemLinks = !!allItemsLinkedToItem.find((item) => link.item.uuid === item.item.uuid)
const existsInNotesLinkingToItem = !!notesLinkingToActiveItem.find((item) => link.item.uuid === item.item.uuid) const existsInNotesLinkingToItem = !!notesLinkingToItem.find((item) => link.item.uuid === item.item.uuid)
const existsInFilesLinkingToItem = !!filesLinkingToActiveItem.find((item) => link.item.uuid === item.item.uuid) const existsInFilesLinkingToItem = !!filesLinkingToItem.find((item) => link.item.uuid === item.item.uuid)
return ( return (
existsInAllItemLinks && existsInAllItemLinks &&
@@ -94,16 +101,20 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
) )
} }
if (!activeItem) {
return null
}
return ( return (
<div <div
className={classNames( className={classNames(
'note-view-linking-container hidden min-w-80 max-w-full flex-wrap items-center gap-2 bg-transparent md:-mr-2 md:flex', 'note-view-linking-container hidden min-w-80 max-w-full flex-wrap items-center gap-2 bg-transparent md:-mr-2 md:flex',
allItemLinks.length || notesLinkingToActiveItem.length ? 'mt-1' : 'mt-0.5', allItemsLinkedToItem.length || notesLinkingToItem.length ? 'mt-1' : 'mt-0.5',
)} )}
> >
{allItemLinks {allItemsLinkedToItem
.concat(notesLinkingToActiveItem) .concat(notesLinkingToItem)
.concat(filesLinkingToActiveItem) .concat(filesLinkingToItem)
.map((link) => ( .map((link) => (
<LinkedItemBubble <LinkedItemBubble
link={link} link={link}
@@ -123,6 +134,7 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
focusPreviousItem={focusPreviousItem} focusPreviousItem={focusPreviousItem}
setFocusedId={setFocusedId} setFocusedId={setFocusedId}
hoverLabel={`Focus input to add a link (${shortcut})`} hoverLabel={`Focus input to add a link (${shortcut})`}
item={activeItem}
/> />
</div> </div>
) )

View File

@@ -10,22 +10,24 @@ import { useCallback } from 'react'
type Props = { type Props = {
createAndAddNewTag: LinkingController['createAndAddNewTag'] createAndAddNewTag: LinkingController['createAndAddNewTag']
linkItemToSelectedItem: LinkingController['linkItemToSelectedItem'] linkItems: LinkingController['linkItems']
results: LinkableItem[] results: LinkableItem[]
searchQuery: string searchQuery: string
shouldShowCreateTag: boolean shouldShowCreateTag: boolean
onClickCallback?: () => void onClickCallback?: () => void
isEntitledToNoteLinking: boolean isEntitledToNoteLinking: boolean
item: LinkableItem
} }
const LinkedItemSearchResults = ({ const LinkedItemSearchResults = ({
createAndAddNewTag, createAndAddNewTag,
linkItemToSelectedItem, linkItems,
results, results,
searchQuery, searchQuery,
shouldShowCreateTag, shouldShowCreateTag,
onClickCallback, onClickCallback,
isEntitledToNoteLinking, isEntitledToNoteLinking,
item,
}: Props) => { }: Props) => {
const onClickAddNew = useCallback( const onClickAddNew = useCallback(
(searchQuery: string) => { (searchQuery: string) => {
@@ -44,7 +46,7 @@ const LinkedItemSearchResults = ({
key={result.uuid} key={result.uuid}
className="flex w-full items-center justify-between gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground focus:bg-info-backdrop" className="flex w-full items-center justify-between gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground focus:bg-info-backdrop"
onClick={() => { onClick={() => {
void linkItemToSelectedItem(result) void linkItems(item, result)
onClickCallback?.() onClickCallback?.()
}} }}
> >

View File

@@ -3,6 +3,7 @@ import { FilesController } from '@/Controllers/FilesController'
import { LinkingController } from '@/Controllers/LinkingController' import { LinkingController } from '@/Controllers/LinkingController'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useRef, useCallback } from 'react' import { useRef, useCallback } from 'react'
import { useApplication } from '../ApplicationProvider'
import RoundIconButton from '../Button/RoundIconButton' import RoundIconButton from '../Button/RoundIconButton'
import Popover from '../Popover/Popover' import Popover from '../Popover/Popover'
import StyledTooltip from '../StyledTooltip/StyledTooltip' import StyledTooltip from '../StyledTooltip/StyledTooltip'
@@ -16,6 +17,9 @@ type Props = {
} }
const LinkedItemsButton = ({ linkingController, filesController, onClickPreprocessing, featuresController }: Props) => { const LinkedItemsButton = ({ linkingController, filesController, onClickPreprocessing, featuresController }: Props) => {
const application = useApplication()
const activeItem = application.itemControllerGroup.activeItemViewController?.item
const { isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController const { isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
@@ -27,6 +31,10 @@ const LinkedItemsButton = ({ linkingController, filesController, onClickPreproce
setIsLinkingPanelOpen(willMenuOpen) setIsLinkingPanelOpen(willMenuOpen)
}, [isLinkingPanelOpen, onClickPreprocessing, setIsLinkingPanelOpen]) }, [isLinkingPanelOpen, onClickPreprocessing, setIsLinkingPanelOpen])
if (!activeItem) {
return null
}
return ( return (
<> <>
<StyledTooltip label="Linked items panel"> <StyledTooltip label="Linked items panel">
@@ -34,6 +42,7 @@ const LinkedItemsButton = ({ linkingController, filesController, onClickPreproce
</StyledTooltip> </StyledTooltip>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isLinkingPanelOpen} className="pb-2"> <Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isLinkingPanelOpen} className="pb-2">
<LinkedItemsPanel <LinkedItemsPanel
item={activeItem}
isOpen={isLinkingPanelOpen} isOpen={isLinkingPanelOpen}
linkingController={linkingController} linkingController={linkingController}
filesController={filesController} filesController={filesController}

View File

@@ -12,44 +12,45 @@ import Icon from '../Icon/Icon'
import DecoratedInput from '../Input/DecoratedInput' import DecoratedInput from '../Input/DecoratedInput'
import LinkedItemSearchResults from './LinkedItemSearchResults' import LinkedItemSearchResults from './LinkedItemSearchResults'
import { LinkedItemsSectionItem } from './LinkedItemsSectionItem' import { LinkedItemsSectionItem } from './LinkedItemsSectionItem'
import { DecryptedItem } from '@standardnotes/snjs'
const LinkedItemsPanel = ({ const LinkedItemsPanel = ({
linkingController, linkingController,
filesController, filesController,
featuresController, featuresController,
isOpen, isOpen,
item,
}: { }: {
linkingController: LinkingController linkingController: LinkingController
filesController: FilesController filesController: FilesController
featuresController: FeaturesController featuresController: FeaturesController
isOpen: boolean isOpen: boolean
item: DecryptedItem
}) => { }) => {
const { const {
tags, getLinkedTagsForItem,
linkedFiles, getFilesLinksForItem,
filesLinkingToActiveItem, getLinkedNotesForItem,
notesLinkedToItem, getNotesLinkingToItem,
notesLinkingToActiveItem, linkItems,
allItemLinks: allLinkedItems,
linkItemToSelectedItem,
unlinkItemFromSelectedItem, unlinkItemFromSelectedItem,
activateItem, activateItem,
createAndAddNewTag, createAndAddNewTag,
isEntitledToNoteLinking, isEntitledToNoteLinking,
activeItem,
} = linkingController } = linkingController
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
const { filesLinkedToItem, filesLinkingToItem } = getFilesLinksForItem(item)
const notesLinkedToItem = getLinkedNotesForItem(item) || []
const notesLinkingToItem = getNotesLinkingToItem(item) || []
const { entitledToFiles } = featuresController const { entitledToFiles } = featuresController
const application = useApplication() const application = useApplication()
const searchInputRef = useRef<HTMLInputElement | null>(null) const searchInputRef = useRef<HTMLInputElement | null>(null)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const isSearching = !!searchQuery.length const isSearching = !!searchQuery.length
const { linkedResults, unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults( const { linkedResults, unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, item)
searchQuery,
application,
activeItem,
)
useEffect(() => { useEffect(() => {
if (isOpen && searchInputRef.current) { if (isOpen && searchInputRef.current) {
@@ -64,7 +65,7 @@ const LinkedItemsPanel = ({
} }
void filesController.selectAndUploadNewFiles((file) => { void filesController.selectAndUploadNewFiles((file) => {
void linkItemToSelectedItem(file) void linkItems(item, file)
}) })
} }
@@ -73,7 +74,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 || unlinkedItems.length || notesLinkingToActiveItem.length linkedResults.length || unlinkedItems.length || notesLinkingToItem.length
? 'border-b border-border pb-2.5' ? 'border-b border-border pb-2.5'
: 'pb-1', : 'pb-1',
)} )}
@@ -105,7 +106,7 @@ const LinkedItemsPanel = ({
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Unlinked</div> <div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Unlinked</div>
<LinkedItemSearchResults <LinkedItemSearchResults
createAndAddNewTag={createAndAddNewTag} createAndAddNewTag={createAndAddNewTag}
linkItemToSelectedItem={linkItemToSelectedItem} linkItems={linkItems}
results={unlinkedItems} results={unlinkedItems}
searchQuery={searchQuery} searchQuery={searchQuery}
shouldShowCreateTag={shouldShowCreateTag} shouldShowCreateTag={shouldShowCreateTag}
@@ -114,6 +115,7 @@ const LinkedItemsPanel = ({
setSearchQuery('') setSearchQuery('')
searchInputRef.current?.focus() searchInputRef.current?.focus()
}} }}
item={item}
/> />
</div> </div>
)} )}
@@ -137,11 +139,11 @@ const LinkedItemsPanel = ({
</> </>
) : ( ) : (
<> <>
{!!tags.length && ( {!!tagsLinkedToItem.length && (
<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((link) => ( {tagsLinkedToItem.map((link) => (
<LinkedItemsSectionItem <LinkedItemsSectionItem
key={link.id} key={link.id}
item={link.item} item={link.item}
@@ -165,7 +167,7 @@ const LinkedItemsPanel = ({
<Icon type="add" /> <Icon type="add" />
Upload and link file(s) Upload and link file(s)
</button> </button>
{linkedFiles.map((link) => ( {filesLinkedToItem.map((link) => (
<LinkedItemsSectionItem <LinkedItemsSectionItem
key={link.id} key={link.id}
item={link.item} item={link.item}
@@ -178,13 +180,13 @@ const LinkedItemsPanel = ({
</div> </div>
</div> </div>
{!!filesLinkingToActiveItem.length && ( {!!filesLinkingToItem.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">
Files Linking To Current File Files Linking To Current File
</div> </div>
<div className="my-1"> <div className="my-1">
{filesLinkingToActiveItem.map((link) => ( {filesLinkingToItem.map((link) => (
<LinkedItemsSectionItem <LinkedItemsSectionItem
key={link.id} key={link.id}
item={link.item} item={link.item}
@@ -214,13 +216,13 @@ const LinkedItemsPanel = ({
</div> </div>
</div> </div>
)} )}
{!!notesLinkingToActiveItem.length && ( {!!notesLinkingToItem.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">
{notesLinkingToActiveItem.map((link) => ( {notesLinkingToItem.map((link) => (
<LinkedItemsSectionItem <LinkedItemsSectionItem
key={link.id} key={link.id}
item={link.item} item={link.item}

View File

@@ -245,8 +245,6 @@ export class ItemListController extends AbstractViewController implements Intern
await this.application.itemControllerGroup.createItemController({ note }) await this.application.itemControllerGroup.createItemController({ note })
this.linkingController.reloadAllLinks()
await this.publishCrossControllerEventSync(CrossControllerEvent.ActiveEditorChanged) await this.publishCrossControllerEventSync(CrossControllerEvent.ActiveEditorChanged)
} }
@@ -262,8 +260,6 @@ export class ItemListController extends AbstractViewController implements Intern
} }
await this.application.itemControllerGroup.createItemController({ file }) await this.application.itemControllerGroup.createItemController({ file })
this.linkingController.reloadAllLinks()
} }
setCompletedFullSync = (completed: boolean) => { setCompletedFullSync = (completed: boolean) => {
@@ -659,8 +655,6 @@ export class ItemListController extends AbstractViewController implements Intern
const controller = await this.createNewNoteController(useTitle, createdAt, autofocusBehavior) const controller = await this.createNewNoteController(useTitle, createdAt, autofocusBehavior)
this.linkingController.reloadAllLinks()
this.selectionController.scrollToItem(controller.item) this.selectionController.scrollToItem(controller.item)
} }

View File

@@ -4,7 +4,6 @@ import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewCon
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata' import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
import { PrefDefaults } from '@/Constants/PrefDefaults' import { PrefDefaults } from '@/Constants/PrefDefaults'
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem' import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem' import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { import {
ApplicationEvent, ApplicationEvent,
@@ -28,11 +27,6 @@ import { SelectedItemsController } from './SelectedItemsController'
import { SubscriptionController } from './Subscription/SubscriptionController' import { SubscriptionController } from './Subscription/SubscriptionController'
export class LinkingController extends AbstractViewController { export class LinkingController extends AbstractViewController {
tags: ItemLink<SNTag>[] = []
linkedFiles: ItemLink<FileItem>[] = []
filesLinkingToActiveItem: ItemLink<FileItem>[] = []
notesLinkedToItem: ItemLink<SNNote>[] = []
notesLinkingToActiveItem: ItemLink<SNNote>[] = []
shouldLinkToParentFolders: boolean shouldLinkToParentFolders: boolean
isLinkingPanelOpen = false isLinkingPanelOpen = false
private itemListController!: ItemListController private itemListController!: ItemListController
@@ -48,22 +42,11 @@ export class LinkingController extends AbstractViewController {
super(application, eventBus) super(application, eventBus)
makeObservable(this, { makeObservable(this, {
tags: observable,
linkedFiles: observable,
filesLinkingToActiveItem: observable,
notesLinkedToItem: observable,
notesLinkingToActiveItem: observable,
isLinkingPanelOpen: observable, isLinkingPanelOpen: observable,
allItemLinks: computed,
isEntitledToNoteLinking: computed, isEntitledToNoteLinking: computed,
selectedItemTitle: computed,
setIsLinkingPanelOpen: action, setIsLinkingPanelOpen: action,
reloadLinkedFiles: action,
reloadLinkedTags: action,
reloadLinkedNotes: action,
reloadNotesLinkingToItem: action,
}) })
this.shouldLinkToParentFolders = application.getPreference( this.shouldLinkToParentFolders = application.getPreference(
@@ -89,19 +72,6 @@ export class LinkingController extends AbstractViewController {
this.itemListController = itemListController this.itemListController = itemListController
this.filesController = filesController this.filesController = filesController
this.subscriptionController = subscriptionController this.subscriptionController = subscriptionController
this.disposers.push(
this.application.streamItems(ContentType.File, () => {
this.reloadLinkedFiles()
}),
this.application.streamItems(ContentType.Tag, () => {
this.reloadLinkedTags()
}),
this.application.streamItems(ContentType.Note, () => {
this.reloadLinkedNotes()
this.reloadNotesLinkingToItem()
}),
)
} }
get isEntitledToNoteLinking() { get isEntitledToNoteLinking() {
@@ -112,87 +82,61 @@ export class LinkingController extends AbstractViewController {
this.isLinkingPanelOpen = open this.isLinkingPanelOpen = open
} }
get allItemLinks() {
return [...this.tags, ...this.linkedFiles, ...this.notesLinkedToItem]
}
get activeItem() { get activeItem() {
return this.application.itemControllerGroup.activeItemViewController?.item return this.application.itemControllerGroup.activeItemViewController?.item
} }
get selectedItemTitle() { getFilesLinksForItem = (item: LinkableItem | undefined) => {
return this.selectionController.firstSelectedItem if (!item || this.application.items.isTemplateItem(item)) {
? this.selectionController.firstSelectedItem.title return {
: this.activeItem filesLinkedToItem: [],
? this.activeItem.title filesLinkingToItem: [],
: '' }
}
reloadAllLinks() {
this.reloadLinkedFiles()
this.reloadLinkedTags()
this.reloadLinkedNotes()
this.reloadNotesLinkingToItem()
}
reloadLinkedFiles() {
if (!this.activeItem || this.application.items.isTemplateItem(this.activeItem)) {
this.linkedFiles = []
this.filesLinkingToActiveItem = []
return
} }
const referencesOfActiveItem = naturalSort( const referencesOfItem = naturalSort(this.application.items.referencesForItem(item).filter(isFile), 'title')
this.application.items.referencesForItem(this.activeItem).filter(isFile),
'title',
)
const referencingActiveItem = naturalSort( const referencingItem = naturalSort(this.application.items.itemsReferencingItem(item).filter(isFile), 'title')
this.application.items.itemsReferencingItem(this.activeItem).filter(isFile),
'title',
)
if (this.activeItem.content_type === ContentType.File) { if (item.content_type === ContentType.File) {
this.linkedFiles = referencesOfActiveItem.map((item) => createLinkFromItem(item, 'linked')) return {
this.filesLinkingToActiveItem = referencingActiveItem.map((item) => createLinkFromItem(item, 'linked-by')) filesLinkedToItem: referencesOfItem.map((item) => createLinkFromItem(item, 'linked')),
filesLinkingToItem: referencingItem.map((item) => createLinkFromItem(item, 'linked-by')),
}
} else { } else {
this.linkedFiles = referencingActiveItem.map((item) => createLinkFromItem(item, 'linked')) return {
this.filesLinkingToActiveItem = referencesOfActiveItem.map((item) => createLinkFromItem(item, 'linked-by')) filesLinkedToItem: referencingItem.map((item) => createLinkFromItem(item, 'linked')),
filesLinkingToItem: referencesOfItem.map((item) => createLinkFromItem(item, 'linked-by')),
}
} }
} }
reloadLinkedTags() { getLinkedTagsForItem = (item: LinkableItem | undefined) => {
if (!this.activeItem) { if (!item) {
return return
} }
this.tags = this.application.items return this.application.items.getSortedTagsForItem(item).map((tag) => createLinkFromItem(tag, 'linked'))
.getSortedTagsForItem(this.activeItem)
.map((item) => createLinkFromItem(item, 'linked'))
} }
reloadLinkedNotes() { getLinkedNotesForItem = (item: LinkableItem | undefined) => {
if (!this.activeItem || this.application.items.isTemplateItem(this.activeItem)) { if (!item || this.application.items.isTemplateItem(item)) {
this.notesLinkedToItem = [] return []
return
} }
this.notesLinkedToItem = naturalSort( return naturalSort(this.application.items.referencesForItem(item).filter(isNote), 'title').map((item) =>
this.application.items.referencesForItem(this.activeItem).filter(isNote), createLinkFromItem(item, 'linked'),
'title', )
).map((item) => createLinkFromItem(item, 'linked'))
} }
reloadNotesLinkingToItem() { getNotesLinkingToItem = (item: LinkableItem | undefined) => {
if (!this.activeItem) { if (!item) {
this.notesLinkingToActiveItem = [] return []
return
} }
this.notesLinkingToActiveItem = naturalSort( return naturalSort(this.application.items.itemsReferencingItem(item).filter(isNote), 'title').map((item) =>
this.application.items.itemsReferencingItem(this.activeItem).filter(isNote), createLinkFromItem(item, 'linked-by'),
'title', )
).map((item) => createLinkFromItem(item, 'linked-by'))
} }
activateItem = async (item: LinkableItem): Promise<AppPaneId | undefined> => { activateItem = async (item: LinkableItem): Promise<AppPaneId | undefined> => {
@@ -230,7 +174,6 @@ export class LinkingController extends AbstractViewController {
await this.application.items.unlinkItems(selectedItem, itemToUnlink) await this.application.items.unlinkItems(selectedItem, itemToUnlink)
void this.application.sync.sync() void this.application.sync.sync()
this.reloadAllLinks()
} }
ensureActiveItemIsInserted = async () => { ensureActiveItemIsInserted = async () => {
@@ -240,11 +183,22 @@ export class LinkingController extends AbstractViewController {
} }
} }
linkItems = async (item: SNNote | FileItem, itemToLink: LinkableItem) => { linkItems = async (item: LinkableItem, itemToLink: LinkableItem) => {
if (item instanceof SNNote) { if (item instanceof SNNote) {
if (itemToLink instanceof SNNote && !this.isEntitledToNoteLinking) {
void this.publishCrossControllerEventSync(CrossControllerEvent.DisplayPremiumModal, {
featureName: 'Note linking',
})
return
}
if (item.uuid === this.activeItem?.uuid) {
await this.ensureActiveItemIsInserted()
}
if (itemToLink instanceof FileItem) { if (itemToLink instanceof FileItem) {
await this.application.items.associateFileWithNote(itemToLink, item) await this.application.items.associateFileWithNote(itemToLink, item)
} else if (itemToLink instanceof SNNote && this.isEntitledToNoteLinking) { } else if (itemToLink instanceof SNNote) {
await this.application.items.linkNoteToNote(item, itemToLink) await this.application.items.linkNoteToNote(item, itemToLink)
} else if (itemToLink instanceof SNTag) { } else if (itemToLink instanceof SNTag) {
await this.addTagToItem(itemToLink, item) await this.addTagToItem(itemToLink, item)
@@ -266,7 +220,6 @@ export class LinkingController extends AbstractViewController {
} }
void this.application.sync.sync() void this.application.sync.sync()
this.reloadAllLinks()
} }
linkItemToSelectedItem = async (itemToLink: LinkableItem): Promise<boolean> => { linkItemToSelectedItem = async (itemToLink: LinkableItem): Promise<boolean> => {
@@ -307,7 +260,6 @@ export class LinkingController extends AbstractViewController {
await this.application.items.addTagToFile(item, tag, this.shouldLinkToParentFolders) await this.application.items.addTagToFile(item, tag, this.shouldLinkToParentFolders)
} }
this.reloadLinkedTags()
this.application.sync.sync().catch(console.error) this.application.sync.sync().catch(console.error)
} }
} }

View File

@@ -0,0 +1,33 @@
import { useApplication } from '@/Components/ApplicationProvider'
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
import { ContentType, DecryptedItem } from '@standardnotes/snjs'
import { useEffect, useState } from 'react'
export const useItemLinks = (item: DecryptedItem | undefined) => {
const application = useApplication()
const linkingController = useLinkingController()
const { getLinkedNotesForItem, getNotesLinkingToItem, getFilesLinksForItem, getLinkedTagsForItem } = linkingController
const [, refresh] = useState(Date.now())
const notesLinkedToItem = getLinkedNotesForItem(item) || []
const notesLinkingToItem = getNotesLinkingToItem(item) || []
const { filesLinkedToItem, filesLinkingToItem } = getFilesLinksForItem(item)
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
useEffect(
() =>
application.streamItems([ContentType.Note, ContentType.File, ContentType.Tag], () => {
refresh(Date.now())
}),
[application],
)
return {
notesLinkedToItem,
notesLinkingToItem,
filesLinkedToItem,
filesLinkingToItem,
tagsLinkedToItem,
}
}