import { Action, createAction, useKBar, useRegisterActions } from 'kbar' import debounce from 'lodash/debounce' import { useRouter } from 'next/router' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Toaster } from 'react-hot-toast' import TopBarProgress from 'react-topbar-progress-indicator' import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll' import { usePersistedState } from '../../../lib/hooks/usePersistedState' import { libraryListCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts' import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts' import { SearchItem, TypeaheadSearchItemsData, typeaheadSearchQuery, } from '../../../lib/networking/queries/typeaheadSearch' import { LibraryItem, LibraryItemNode, LibraryItems, LibraryItemsQueryInput, useArchiveItem, useBulkActions, useDeleteItem, useGetLibraryItems, useMoveItemToFolder, useRefreshProcessingItems, useUpdateItemReadStatus, } from '../../../lib/networking/library_items/useLibraryItems' import { useGetViewerQuery, UserBasicData, } from '../../../lib/networking/queries/useGetViewerQuery' import { Button } from '../../elements/Button' import { StyledText } from '../../elements/StyledText' import { ConfirmationModal } from '../../patterns/ConfirmationModal' import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes' import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { AddLinkModal } from '../AddLinkModal' import { EditLibraryItemModal } from '../homeFeed/EditItemModals' import { EmptyLibrary } from '../homeFeed/EmptyLibrary' import { MultiSelectMode } from '../homeFeed/LibraryHeader' import { UploadModal } from '../UploadModal' import { BulkAction } from '../../../lib/networking/library_items/useLibraryItems' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter' import { NotebookPresenter } from '../article/NotebookPresenter' import { PinnedButtons } from '../homeFeed/PinnedButtons' import { PinnedSearch } from '../../../pages/settings/pinned-searches' import { FetchItemsError } from '../homeFeed/FetchItemsError' import { LibraryHeader } from './LibraryHeader' import { TrashIcon } from '../../elements/icons/TrashIcon' import { theme } from '../../tokens/stitches.config' import { emptyTrashMutation } from '../../../lib/networking/mutations/emptyTrashMutation' import { State } from '../../../lib/networking/fragments/articleFragment' import { useHandleAddUrl } from '../../../lib/hooks/useHandleAddUrl' import { QueryClient, useQueryClient } from '@tanstack/react-query' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' const fetchSearchResults = async (query: string, cb: any) => { if (!query.startsWith('#')) return const res = await typeaheadSearchQuery({ limit: 10, searchQuery: query.substring(1), }) cb(res) } const debouncedFetchSearchResults = debounce((query, cb) => { fetchSearchResults(query, cb) }, 300) type LibraryContainerProps = { folder: string | undefined filterFunc: (item: LibraryItemNode) => boolean showNavigationMenu: boolean } export function LibraryContainer(props: LibraryContainerProps): JSX.Element { const router = useRouter() const { viewerData } = useGetViewerQuery() const { queryValue } = useKBar((state) => ({ queryValue: state.searchQuery })) const [searchResults, setSearchResults] = useState([]) const defaultQuery = { limit: 10, folder: props.folder, sortDescending: true, searchQuery: undefined, } const gridContainerRef = useRef(null) const [labelsTarget, setLabelsTarget] = useState(undefined) const [notebookTarget, setNotebookTarget] = useState(undefined) const [showAddLinkModal, setShowAddLinkModal] = useState(false) const [showEditTitleModal, setShowEditTitleModal] = useState(false) const [linkToEdit, setLinkToEdit] = useState() const [linkToUnsubscribe, setLinkToUnsubscribe] = useState() const archiveItem = useArchiveItem() const deleteItem = useDeleteItem() const moveToFolder = useMoveItemToFolder() const bulkAction = useBulkActions() const updateItemReadStatus = useUpdateItemReadStatus() const [queryInputs, setQueryInputs] = useState(defaultQuery) const { data: itemsPages, isLoading, fetchNextPage, hasNextPage, error: fetchItemsError, } = useGetLibraryItems(props.folder, queryInputs) useEffect(() => { if (queryValue.startsWith('#')) { debouncedFetchSearchResults( queryValue, (data: TypeaheadSearchItemsData) => { setSearchResults(data?.typeaheadSearch.items || []) } ) } else setSearchResults([]) }, [queryValue]) useEffect(() => { if (!router.isReady) return const q = router.query['q'] let qs = '' if (q && typeof q === 'string') { qs = q } if (qs !== (queryInputs.searchQuery || '')) { setQueryInputs({ ...queryInputs, searchQuery: qs }) // performActionOnItem('refresh', undefined as unknown as any) } // intentionally not watching queryInputs and performActionOnItem // here to prevent infinite looping // eslint-disable-next-line react-hooks/exhaustive-deps }, [setQueryInputs, router.isReady, router.query]) useEffect(() => { window.localStorage.setItem('nav-return', router.asPath) }, [router.asPath]) const libraryItems = useMemo(() => { const items = itemsPages?.pages .flatMap((ad: LibraryItems) => { return ad.edges.map((it) => ({ ...it, isLoading: it.node.state === 'PROCESSING', })) }) .filter((item) => props.filterFunc(item.node)) || [] return items }, [itemsPages]) useEffect(() => { if (localStorage) { localStorage.setItem( 'library-slug-list', JSON.stringify(libraryItems.map((li) => li.node.slug)) ) } }, [libraryItems]) const processingItems = useMemo(() => { return libraryItems .filter((li) => li.node.state === State.PROCESSING) .map((li) => li.node.id) }, [libraryItems]) const refreshProcessingItems = useRefreshProcessingItems() useEffect(() => { if (processingItems.length) { refreshProcessingItems.mutateAsync({ attempt: 0, itemIds: processingItems, }) } }, [processingItems]) const focusFirstItem = useCallback(() => { if (libraryItems.length < 1) { return } const firstItem = libraryItems[0] if (!firstItem) { return } const firstItemElement = document.getElementById(firstItem.node.id) if (!firstItemElement) { return } activateCard(firstItem.node.id) }, [libraryItems]) const activateCard = useCallback( (id: string) => { if (!document.getElementById(id)) { return } setActiveCardId(id) scrollToActiveCard(id, true) const newItem = getItem(id) if (notebookTarget && newItem) { setNotebookTarget(newItem) } }, [libraryItems] ) const isVisible = function (ele: HTMLElement) { const container = window.document.documentElement const eleTop = ele.offsetTop const eleBottom = eleTop + ele.clientHeight const containerTop = container.scrollTop + 200 const containerBottom = containerTop + container.clientHeight return eleTop >= containerTop && eleBottom <= containerBottom } const scrollToActiveCard = useCallback( (id: string | null, isSmouth?: boolean): void => { if (id) { const target = document.getElementById(id) if (target) { try { if (!isVisible(target)) { target.scrollIntoView({ block: 'center', behavior: isSmouth ? 'smooth' : 'auto', }) } target.focus({ preventScroll: true, }) } catch (error) { console.log('Cannot Scroll', error) } } } }, [] ) const alreadyScrolled = useRef(false) const [activeCardId, setActiveCardId] = usePersistedState({ key: `--library-active-card-id`, initialValue: null, isSessionStorage: true, }) const activeItem = useMemo(() => { if (!activeCardId) { return undefined } return libraryItems.find((item) => item.node.id === activeCardId) }, [libraryItems, activeCardId]) const getItem = useCallback( (itemId: string) => { return libraryItems.find((item) => item.node.id === itemId) }, [libraryItems] ) const activeItemIndex = useMemo(() => { if (!activeCardId) { return undefined } const result = libraryItems.findIndex( (item) => item.node.id === activeCardId ) return result >= 0 ? result : undefined }, [libraryItems, activeCardId]) useEffect(() => { if (activeCardId && !alreadyScrolled.current) { scrollToActiveCard(activeCardId) alreadyScrolled.current = true } }, [activeCardId, scrollToActiveCard]) const handleCardAction = async ( action: LinkedItemCardAction, item: LibraryItem | undefined ): Promise => { if (!item) { return } switch (action) { case 'showDetail': const username = viewerData?.me?.profile.username if (username) { setActiveCardId(item.node.id) if (item.node.state === State.PROCESSING) { router.push(`/article?url=${encodeURIComponent(item.node.url)}`) } else { router.push(`/${username}/${item.node.slug}`) } } break case 'showOriginal': const url = item.node.originalArticleUrl if (url) { window.open(url, '_blank') } break case 'archive': case 'unarchive': try { await archiveItem.mutateAsync({ itemId: item.node.id, slug: item.node.slug, input: { linkId: item.node.id, archived: action == 'archive', }, }) } catch (err) { console.log('Error setting archive state: ', err) showErrorToast(`Error ${action}ing item`, { position: 'bottom-right', }) return } showSuccessToast(`Item ${action}d`, { position: 'bottom-right', }) break case 'delete': try { await deleteItem.mutateAsync({ itemId: item.node.id, slug: item.node.slug, }) } catch (err) { console.log('Error deleting item: ', err) showErrorToast(`Error deleting item`, { position: 'bottom-right', }) return } showSuccessToast(`Item deleted`, { position: 'bottom-right', }) break case 'mark-read': case 'mark-unread': const desc = action == 'mark-read' ? 'read' : 'unread' const values = action == 'mark-read' ? { readingProgressPercent: 100, readingProgressTopPercent: 100, readingProgressAnchorIndex: 0, } : { readingProgressPercent: 0, readingProgressTopPercent: 0, readingProgressAnchorIndex: 0, } try { await updateItemReadStatus.mutateAsync({ itemId: item.node.id, slug: item.node.slug, input: { id: item.node.id, force: true, ...values, }, }) } catch (err) { console.log('Error marking item: ', err) showErrorToast(`Error marking as ${desc}`, { position: 'bottom-right', }) return } break case 'move-to-inbox': try { await moveToFolder.mutateAsync({ itemId: item.node.id, slug: item.node.slug, folder: 'inbox', }) } catch (err) { console.log('Error moving item: ', err) showErrorToast(`Error moving item`, { position: 'bottom-right', }) return } showSuccessToast(`Item moved to library`, { position: 'bottom-right', }) break case 'set-labels': setLabelsTarget(item) break case 'open-notebook': if (!notebookTarget) { setNotebookTarget(item) } else { setNotebookTarget(undefined) } break case 'unsubscribe': // setLinkToUnsubscribe(item.node) break default: console.warn('unknown action: ', action) } } const modalTargetItem = useMemo(() => { return labelsTarget || linkToEdit || linkToUnsubscribe }, [labelsTarget, linkToEdit, linkToUnsubscribe]) const [checkedItems, setCheckedItems] = useState([]) const [multiSelectMode, setMultiSelectMode] = useState('off') const selectActiveArticle = useCallback(() => { if (activeItem) { if (multiSelectMode === 'off') { setMultiSelectMode('some') } const itemId = activeItem.node.id const isChecked = itemIsChecked(itemId) setIsChecked(itemId, !isChecked) } }, [activeItem, multiSelectMode, checkedItems]) useKeyboardShortcuts( libraryListCommands((action) => { const columnCount = (container: HTMLDivElement) => { const gridComputedStyle = window.getComputedStyle(container) const gridColumnCount = gridComputedStyle .getPropertyValue('grid-template-columns') .split(' ').length return gridColumnCount } // If any of the modals are open we disable handling keyboard shortcuts if (modalTargetItem) { return } switch (action) { case 'openArticle': if (multiSelectMode !== 'off' && activeItem) { const itemId = activeItem.node.id const isChecked = itemIsChecked(itemId) setIsChecked(itemId, !isChecked) } else { handleCardAction('showDetail', activeItem) } break case 'selectArticle': selectActiveArticle() break case 'openOriginalArticle': handleCardAction('showOriginal', activeItem) break case 'showAddLinkModal': setTimeout(() => setShowAddLinkModal(true), 0) break case 'moveFocusToNextListItem': { const currentItemIndex = activeItemIndex const nextItemIndex = currentItemIndex == undefined ? 0 : currentItemIndex + 1 const nextItem = libraryItems[nextItemIndex] if (nextItem) { activateCard(nextItem.node.id) } break } case 'moveFocusToPreviousListItem': { const currentItemIndex = activeItemIndex const previousItemIndex = currentItemIndex == undefined ? 0 : currentItemIndex - 1 const previousItem = libraryItems[previousItemIndex] if (previousItem) { activateCard(previousItem.node.id) } break } case 'moveFocusToNextRowItem': { const selectedItemIndex = activeItemIndex if (selectedItemIndex !== undefined && gridContainerRef?.current) { const nextItemIndex = Math.min( selectedItemIndex + columnCount(gridContainerRef.current), libraryItems.length - 1 ) const nextItem = libraryItems[nextItemIndex] if (nextItem) { const nextItemElement = document.getElementById(nextItem.node.id) if (nextItemElement) { activateCard(nextItem.node.id) } } } else { focusFirstItem() } break } case 'moveFocusToPreviousRowItem': { const selectedItemIndex = activeItemIndex if (selectedItemIndex !== undefined && gridContainerRef?.current) { const nextItemIndex = Math.max( 0, selectedItemIndex - columnCount(gridContainerRef.current) ) const nextItem = libraryItems[nextItemIndex] if (nextItem) { const nextItemElement = document.getElementById(nextItem.node.id) if (nextItemElement) { activateCard(nextItem.node.id) } } } else { focusFirstItem() } break } case 'archiveItem': handleCardAction('archive', activeItem) break case 'removeItem': handleCardAction('delete', activeItem) break case 'markItemAsRead': handleCardAction('mark-read', activeItem) break case 'markItemAsUnread': handleCardAction('mark-unread', activeItem) break case 'showEditLabelsModal': handleCardAction('set-labels', activeItem) break case 'openNotebook': handleCardAction('open-notebook', activeItem) break case 'sortDescending': setQueryInputs({ ...queryInputs, sortDescending: true }) break case 'sortAscending': setQueryInputs({ ...queryInputs, sortDescending: false }) break case 'beginMultiSelect': if (multiSelectMode == 'off') { setMultiSelectMode('none') } break case 'endMultiSelect': setMultiSelectMode('off') break } }) ) const ARCHIVE_ACTION = activeItem?.node.state !== State.ARCHIVED ? createAction({ section: 'Library', name: 'Archive selected item', shortcut: ['e'], perform: () => handleCardAction('archive', activeItem), }) : createAction({ section: 'Library', name: 'UnArchive selected item', shortcut: ['e'], perform: () => handleCardAction('unarchive', activeItem), }) const ACTIVE_ACTIONS = [ ARCHIVE_ACTION, createAction({ section: 'Library', name: 'Remove item', shortcut: ['#'], perform: () => handleCardAction('delete', activeItem), }), createAction({ section: 'Library', name: 'Edit item labels', shortcut: ['l'], perform: () => handleCardAction('set-labels', activeItem), }), createAction({ section: 'Library', name: 'Open Notebook', shortcut: ['t'], perform: () => handleCardAction('open-notebook', activeItem), }), createAction({ section: 'Library', name: 'Mark item as read', shortcut: ['-'], perform: () => { handleCardAction('mark-read', activeItem) }, }), createAction({ section: 'Library', name: 'Mark item as unread', shortcut: ['_'], perform: () => handleCardAction('mark-unread', activeItem), }), ] const UNACTIVE_ACTIONS: Action[] = [ // createAction({ // section: 'Library', // name: 'Sort in ascending order', // shortcut: ['s', 'o'], // perform: () => setQueryInputs({ ...queryInputs, sortDescending: false }), // }), // createAction({ // section: 'Library', // name: 'Sort in descending order', // shortcut: ['s', 'n'], // perform: () => setQueryInputs({ ...queryInputs, sortDescending: true }), // }), ] useRegisterActions( searchResults.map((link) => ({ id: link.id, section: 'Search Results', name: link.title, keywords: '#' + link.title + ' #' + link.siteName, perform: () => { const username = viewerData?.me?.profile.username if (username) { setActiveCardId(link.id) router.push(`/${username}/${link.slug}`) } }, })), [searchResults] ) useRegisterActions( activeCardId ? [...ACTIVE_ACTIONS, ...UNACTIVE_ACTIONS] : UNACTIVE_ACTIONS, [activeCardId, activeItem] ) useFetchMore(fetchNextPage) const setIsChecked = useCallback( (itemId: string, set: boolean) => { if (set && checkedItems.indexOf(itemId) === -1) { checkedItems.push(itemId) setCheckedItems([...checkedItems]) } else if (!set && checkedItems.indexOf(itemId) !== -1) { checkedItems.splice(checkedItems.indexOf(itemId), 1) setCheckedItems([...checkedItems]) } if (set && multiSelectMode == 'off') { setMultiSelectMode('some') } if (checkedItems.length < 1) { setMultiSelectMode('off') } }, [checkedItems, multiSelectMode, setMultiSelectMode] ) useEffect(() => { switch (multiSelectMode) { case 'off': case 'none': setCheckedItems([]) break case 'some': break case 'search': case 'visible': const allIds = ( itemsPages?.pages.flatMap((ad) => { return ad.edges }) || [] ).map((item) => item.node.id) setCheckedItems(allIds) break } }, [multiSelectMode]) const itemIsChecked = useCallback( (itemId: string) => { return checkedItems.indexOf(itemId) !== -1 }, [checkedItems] ) const performMultiSelectAction = useCallback( (action: BulkAction, labelIds?: string[]) => { if (multiSelectMode === 'off') { return } if (multiSelectMode !== 'search' && checkedItems.length < 1) { return } ;(async () => { const query = multiSelectMode === 'search' ? queryInputs.searchQuery || 'in:inbox' : `includes:${checkedItems.join(',')}` const expectedCount = checkedItems.length let bulkArguments = undefined if (action == BulkAction.MOVE_TO_FOLDER) { bulkArguments = { folder: 'inbox ' } } try { const res = await bulkAction.mutateAsync({ action, query, expectedCount, labelIds, arguments: bulkArguments, }) if (res) { let successMessage: string | undefined = undefined switch (action) { case BulkAction.ARCHIVE: successMessage = 'Link Archived' break case BulkAction.ADD_LABELS: successMessage = 'Labels Added' break case BulkAction.DELETE: successMessage = 'Items deleted' break case BulkAction.MARK_AS_READ: successMessage = 'Items marked as read' break case BulkAction.MOVE_TO_FOLDER: successMessage = 'Items moved to library' break } if (successMessage) { showSuccessToast(successMessage, { position: 'bottom-right' }) } } else { showErrorToast('Error performing bulk action', { position: 'bottom-right', }) } } catch (err) { showErrorToast('Error performing bulk action', { position: 'bottom-right', }) } // mutate() })() setMultiSelectMode('off') }, [itemsPages, multiSelectMode, checkedItems] ) return ( { setQueryInputs({ ...queryInputs, searchQuery, }) const qp = new URLSearchParams(window.location.search) if (searchQuery) { qp.set('q', searchQuery) } else { qp.delete('q') } const href = `${window.location.pathname}?${qp.toString()}` router.push(href, href, { shallow: true }) window.sessionStorage.setItem('q', qp.toString()) }} loadMore={fetchNextPage} hasMore={hasNextPage ?? false} hasData={!!itemsPages} isValidating={isLoading} fetchItemsError={!!fetchItemsError} labelsTarget={labelsTarget} setLabelsTarget={setLabelsTarget} notebookTarget={notebookTarget} setNotebookTarget={setNotebookTarget} showAddLinkModal={showAddLinkModal} setShowAddLinkModal={setShowAddLinkModal} showEditTitleModal={showEditTitleModal} setShowEditTitleModal={setShowEditTitleModal} setActiveItem={(item: LibraryItem) => { activateCard(item.node.id) }} linkToEdit={linkToEdit} setLinkToEdit={setLinkToEdit} linkToUnsubscribe={linkToUnsubscribe} setLinkToUnsubscribe={setLinkToUnsubscribe} numItemsSelected={checkedItems.length} /> ) } export type HomeFeedContentProps = { folder: string | undefined items: LibraryItem[] searchTerm?: string gridContainerRef: React.RefObject applySearchQuery: (searchQuery: string) => void hasMore: boolean hasData: boolean isValidating: boolean fetchItemsError: boolean loadMore: () => void labelsTarget: LibraryItem | undefined setLabelsTarget: (target: LibraryItem | undefined) => void notebookTarget: LibraryItem | undefined setNotebookTarget: (target: LibraryItem | undefined) => void showAddLinkModal: boolean setShowAddLinkModal: (show: boolean) => void showEditTitleModal: boolean setShowEditTitleModal: (show: boolean) => void setActiveItem: (item: LibraryItem) => void linkToEdit: LibraryItem | undefined setLinkToEdit: (set: LibraryItem | undefined) => void linkToUnsubscribe: LibraryItem | undefined setLinkToUnsubscribe: (set: LibraryItem | undefined) => void actionHandler: ( action: LinkedItemCardAction, item: LibraryItem | undefined ) => Promise showNavigationMenu: boolean setIsChecked: (itemId: string, set: boolean) => void itemIsChecked: (itemId: string) => boolean multiSelectMode: MultiSelectMode setMultiSelectMode: (mode: MultiSelectMode) => void numItemsSelected: number performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void } function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { const { viewerData } = useGetViewerQuery() const [layout, setLayout] = usePersistedState({ key: 'libraryLayout', initialValue: 'LIST_LAYOUT', }) const updateLayout = useCallback( async (newLayout: LayoutType) => { if (layout === newLayout) return setLayout(newLayout) }, [layout, setLayout] ) const showItems = useMemo(() => { if (props.fetchItemsError) { return false } if (!props.isValidating && props.items.length <= 0) { return false } return true }, [props]) const addUrl = useHandleAddUrl() return ( { props.applySearchQuery(searchQuery) }} multiSelectMode={props.multiSelectMode} setMultiSelectMode={props.setMultiSelectMode} numItemsSelected={props.numItemsSelected} performMultiSelectAction={props.performMultiSelectAction} /> {!showItems && props.fetchItemsError && } {!showItems && !props.fetchItemsError && props.items.length <= 0 && ( { props.setShowAddLinkModal(true) }} /> )} {showItems && ( )} {props.showAddLinkModal && ( props.setShowAddLinkModal(false)} /> )} ) } type LibraryItemsLayoutProps = { folder: string | undefined layout: LayoutType viewer?: UserBasicData multiSelectMode: MultiSelectMode setMultiSelectMode: (mode: MultiSelectMode) => void isChecked: (itemId: string) => boolean setIsChecked: (itemId: string, set: boolean) => void } & HomeFeedContentProps export function LibraryItemsLayout( props: LibraryItemsLayoutProps ): JSX.Element { const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] = useState(false) const [showUploadModal, setShowUploadModal] = useState(false) const unsubscribe = () => { if (!props.linkToUnsubscribe) { return } props.actionHandler('unsubscribe', props.linkToUnsubscribe) props.setLinkToUnsubscribe(undefined) setShowUnsubscribeConfirmation(false) } const [pinnedSearches, setPinnedSearches] = usePersistedState< PinnedSearch[] | null >({ key: `--library-pinned-searches`, initialValue: [], isSessionStorage: false, }) return ( <> {props.folder == 'trash' && ( Items that remain in your trash for 14 days will be permanently deleted.
)} {props.isValidating && props.items.length == 0 && }
{ if ( event.dataTransfer.types.find((t) => t.toLowerCase() == 'files') ) { setShowUploadModal(true) } }} style={{ height: '100%', width: '100%' }} > {props.hasMore ? ( ) : ( )}
{props.showEditTitleModal && ( { props.setShowEditTitleModal(false) props.setLinkToEdit(undefined) }} updateItem={async () => { await Promise.resolve() console.log('item updated') }} item={props.linkToEdit as LibraryItem} /> )} {showUnsubscribeConfirmation && ( setShowUnsubscribeConfirmation(false)} /> )} {props.labelsTarget?.node.id && ( { if (props.labelsTarget) { const activate = props.labelsTarget props.setActiveItem(activate) props.setLabelsTarget(undefined) } }} /> )} {props.viewer && props.notebookTarget?.node.id && ( { // onClose={(highlights: Highlight[]) => { // if (props.notebookTarget?.node.highlights) { // props.notebookTarget.node.highlights = highlights // } props.setNotebookTarget(open ? props.notebookTarget : undefined) }} /> )} {showUploadModal && ( setShowUploadModal(false)} /> )} ) } type LibraryItemsProps = { folder: string | undefined items: LibraryItem[] layout: LayoutType viewer: UserBasicData | undefined gridContainerRef: React.RefObject setShowEditTitleModal: (show: boolean) => void setLinkToEdit: (set: LibraryItem | undefined) => void setShowUnsubscribeConfirmation: (show: true) => void setLinkToUnsubscribe: (set: LibraryItem | undefined) => void isChecked: (itemId: string) => boolean setIsChecked: (itemId: string, set: boolean) => void multiSelectMode: MultiSelectMode actionHandler: ( action: LinkedItemCardAction, item: LibraryItem | undefined ) => Promise } function LibraryItemsList(props: LibraryItemsProps): JSX.Element { return ( {props.items.map((linkedItem) => ( div': { // bg: '$thLeftMenuBackground', // bg: '$thLibraryBackground', }, '&:focus': { outline: 'none', '> div': { outline: 'none', bg: '$thBackgroundActive', }, }, '&:hover': { '> div': { bg: '$thBackgroundActive', boxShadow: '$cardBoxShadow', }, '> a': { bg: '$thBackgroundActive', }, }, }} > {props.viewer && ( { if (action === 'editTitle') { props.setShowEditTitleModal(true) props.setLinkToEdit(linkedItem) } else if (action == 'unsubscribe') { props.setShowUnsubscribeConfirmation(true) props.setLinkToUnsubscribe(linkedItem) } else { props.actionHandler(action, linkedItem) } document.body.style.removeProperty('pointer-events') }} /> )} ))} ) }