Files
standardnotes-app-web/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx

270 lines
9.6 KiB
TypeScript

import { FeatureName } from '@/Controllers/FeatureName'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { FilesController } from '@/Controllers/FilesController'
import { LinkingController } from '@/Controllers/LinkingController'
import { classNames } from '@standardnotes/utils'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import ClearInputButton from '../ClearInputButton/ClearInputButton'
import Icon from '../Icon/Icon'
import DecoratedInput from '../Input/DecoratedInput'
import LinkedItemSearchResults from './LinkedItemSearchResults'
import { LinkedItemsSectionItem } from './LinkedItemsSectionItem'
const LinkedItemsPanel = ({
linkingController,
filesController,
featuresController,
isOpen,
}: {
linkingController: LinkingController
filesController: FilesController
featuresController: FeaturesController
isOpen: boolean
}) => {
const {
tags,
linkedFiles,
filesLinkingToActiveItem,
notesLinkedToItem,
notesLinkingToActiveItem,
allItemLinks: allLinkedItems,
linkItemToSelectedItem,
unlinkItemFromSelectedItem,
activateItem,
createAndAddNewTag,
isEntitledToNoteLinking,
activeItem,
} = linkingController
const { entitledToFiles } = featuresController
const application = useApplication()
const fileInputRef = useRef<HTMLInputElement | null>(null)
const searchInputRef = useRef<HTMLInputElement | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const isSearching = !!searchQuery.length
const { linkedResults, unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(
searchQuery,
application,
activeItem,
)
useEffect(() => {
if (isOpen && searchInputRef.current) {
searchInputRef.current.focus()
}
}, [isOpen])
const handleFileInputChange: ChangeEventHandler<HTMLInputElement> = (event) => {
const files = event.currentTarget.files
if (!files) {
return
}
for (let i = 0; i < files.length; i++) {
void filesController.uploadNewFile(files[i]).then((uploadedFiles) => {
if (uploadedFiles) {
void linkItemToSelectedItem(uploadedFiles[0])
}
})
}
}
const selectAndUploadFiles = () => {
if (!entitledToFiles) {
void featuresController.showPremiumAlert(FeatureName.Files)
return
}
if (!fileInputRef.current) {
return
}
fileInputRef.current.click()
}
return (
<div>
<form
className={classNames(
'sticky top-0 z-10 bg-default px-2.5 pt-2.5',
allLinkedItems.length || linkedResults.length || unlinkedItems.length || notesLinkingToActiveItem.length
? 'border-b border-border pb-2.5'
: 'pb-1',
)}
>
<DecoratedInput
type="text"
className={{ container: !isSearching ? 'py-1.5 px-0.5' : 'py-0', input: 'placeholder:text-passive-0' }}
placeholder="Search items to link..."
value={searchQuery}
onChange={setSearchQuery}
ref={searchInputRef}
right={[
isSearching && (
<ClearInputButton
onClick={() => {
setSearchQuery('')
searchInputRef.current?.focus()
}}
/>
),
]}
/>
</form>
<div className="divide-y divide-border">
{isSearching ? (
<>
{(!!unlinkedItems.length || shouldShowCreateTag) && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Unlinked</div>
<LinkedItemSearchResults
createAndAddNewTag={createAndAddNewTag}
linkItemToSelectedItem={linkItemToSelectedItem}
results={unlinkedItems}
searchQuery={searchQuery}
shouldShowCreateTag={shouldShowCreateTag}
isEntitledToNoteLinking={isEntitledToNoteLinking}
onClickCallback={() => {
setSearchQuery('')
searchInputRef.current?.focus()
}}
/>
</div>
)}
{!!linkedResults.length && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked</div>
<div className="my-1">
{linkedResults.map((link) => (
<LinkedItemsSectionItem
key={link.id}
item={link.item}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
))}
</div>
</div>
)}
</>
) : (
<>
{!!tags.length && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Tags</div>
<div className="my-1">
{tags.map((link) => (
<LinkedItemsSectionItem
key={link.id}
item={link.item}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
))}
</div>
</div>
)}
<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">
<input
type="file"
className="absolute top-0 left-0 -z-50 h-px w-px opacity-0"
multiple
ref={fileInputRef}
onChange={handleFileInputChange}
/>
<button
className="flex w-full cursor-pointer items-center gap-3 bg-transparent px-3 py-2 text-left text-base text-text hover:bg-info-backdrop hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={selectAndUploadFiles}
>
<Icon type="add" />
Upload and link file(s)
</button>
{linkedFiles.map((link) => (
<LinkedItemsSectionItem
key={link.id}
item={link.item}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
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}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
))}
</div>
</div>
)}
{!!notesLinkedToItem.length && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Notes</div>
<div className="my-1">
{notesLinkedToItem.map((link) => (
<LinkedItemsSectionItem
key={link.id}
item={link.item}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
))}
</div>
</div>
)}
{!!notesLinkingToActiveItem.length && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">
Notes Linking To This Note
</div>
<div className="my-1">
{notesLinkingToActiveItem.map((link) => (
<LinkedItemsSectionItem
key={link.id}
item={link.item}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
))}
</div>
</div>
)}
</>
)}
</div>
</div>
)
}
export default observer(LinkedItemsPanel)