diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx
index 313f5fff5..e405a5995 100644
--- a/packages/web/components/elements/Button.tsx
+++ b/packages/web/components/elements/Button.tsx
@@ -106,6 +106,7 @@ export const Button = styled('button', {
borderRadius: '$3',
px: '$3',
py: '$2',
+ gap: '5px',
cursor: 'pointer',
border: '1px solid $grayBgSubtle',
bg: '$grayBgSubtle',
@@ -113,6 +114,40 @@ export const Button = styled('button', {
border: '1px solid $grayBorderHover',
},
},
+ outline: {
+ display: 'flex',
+ borderRadius: '5px',
+ alignItems: 'center',
+ px: '15px',
+ py: '5px',
+ gap: '10px',
+ fontSize: '12px',
+ fontWeight: '600',
+ fontFamily: '$inter',
+ cursor: 'pointer',
+ border: '1px solid $thBorderSubtle',
+ bg: 'transparent',
+ '&:hover': {
+ border: '1px solid $grayBorderHover',
+ },
+ },
+ cancel: {
+ display: 'flex',
+ borderRadius: '5px',
+ alignItems: 'center',
+ px: '15px',
+ py: '5px',
+ gap: '10px',
+ fontSize: '12px',
+ fontWeight: '600',
+ fontFamily: '$inter',
+ cursor: 'pointer',
+ bg: '$thBackground4',
+ border: '1px solid $thBackground4',
+ '&:hover': {
+ border: '1px solid $grayBorderHover',
+ },
+ },
modalOption: {
style: 'ghost',
height: '52px',
@@ -277,7 +312,7 @@ export const IconButton = styled(Button, {
height: '28px',
color: '#898989',
border: 'unset',
- background: '$thBackground',
+ background: 'transparent',
boxSizing: 'border-box',
borderRadius: 6,
},
diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx
index b14f3f9a3..ed8528544 100644
--- a/packages/web/components/elements/DropdownElements.tsx
+++ b/packages/web/components/elements/DropdownElements.tsx
@@ -35,6 +35,9 @@ const DropdownTrigger = styled(Trigger, {
'&:hover': {
opacity: 0.7,
},
+ '&:focus': {
+ outline: 'none',
+ },
})
const StyledTriggerItem = styled(TriggerItem, {
diff --git a/packages/web/components/elements/LogoBox.tsx b/packages/web/components/elements/LogoBox.tsx
index 6ea738fe1..d7865ac81 100644
--- a/packages/web/components/elements/LogoBox.tsx
+++ b/packages/web/components/elements/LogoBox.tsx
@@ -31,6 +31,7 @@ export function LogoBox(): JSX.Element {
'@md': {
display: 'none',
},
+ lineHeight: '1',
}}
>
diff --git a/packages/web/components/elements/images/OmnivoreLogoBase.tsx b/packages/web/components/elements/images/OmnivoreLogoBase.tsx
index 9b0c7215d..ecbce303e 100644
--- a/packages/web/components/elements/images/OmnivoreLogoBase.tsx
+++ b/packages/web/components/elements/images/OmnivoreLogoBase.tsx
@@ -25,11 +25,6 @@ export function OmnivoreLogoBase(props: OmnivoreLogoBaseProps): JSX.Element {
onClick={(event) => {
const query = window.sessionStorage.getItem('q')
if (query) {
- console.log(
- 'going to: ',
- window.sessionStorage.getItem('q'),
- props.href
- )
router.push(`/home?${query}`)
event.preventDefault()
}
diff --git a/packages/web/components/patterns/ConfirmationModal.tsx b/packages/web/components/patterns/ConfirmationModal.tsx
index 7ff120c77..f38ed3dc9 100644
--- a/packages/web/components/patterns/ConfirmationModal.tsx
+++ b/packages/web/components/patterns/ConfirmationModal.tsx
@@ -22,7 +22,7 @@ export function ConfirmationModal(props: ConfirmationModalProps): JSX.Element {
-
+
{props.icon ? props.icon : null}
{props.richMessage ? (
props.richMessage
diff --git a/packages/web/components/patterns/LibraryCards/CardTypes.tsx b/packages/web/components/patterns/LibraryCards/CardTypes.tsx
index cf19502e1..2833d724d 100644
--- a/packages/web/components/patterns/LibraryCards/CardTypes.tsx
+++ b/packages/web/components/patterns/LibraryCards/CardTypes.tsx
@@ -19,5 +19,12 @@ export type LinkedItemCardProps = {
item: LibraryItemNode
layout: LayoutType
viewer: UserBasicData
+
handleAction: (action: LinkedItemCardAction) => void
+
+ inMultiSelect: boolean
+ isChecked: boolean
+ setIsChecked: (itemId: string, set: boolean) => void
+
+ isHovered?: boolean
}
diff --git a/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx b/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx
index b224994d7..1775c6143 100644
--- a/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx
+++ b/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx
@@ -146,3 +146,18 @@ export function LibraryItemMetadata(
)
}
+
+type CardCheckBoxProps = {
+ isChecked: boolean
+ handleChanged: () => void
+}
+
+export function CardCheckbox(props: CardCheckBoxProps): JSX.Element {
+ return (
+
+ )
+}
diff --git a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx
index 16c1bbef3..66bf0b96b 100644
--- a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx
+++ b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx
@@ -4,18 +4,18 @@ import type { LinkedItemCardProps } from './CardTypes'
import { CoverImage } from '../../elements/CoverImage'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
-import { useMemo, useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import { DotsThreeVertical } from 'phosphor-react'
import Link from 'next/link'
import { CardMenu } from '../CardMenu'
import {
AuthorInfoStyle,
+ CardCheckbox,
DescriptionStyle,
LibraryItemMetadata,
MenuStyle,
MetaStyle,
siteName,
- timeAgo,
TitleStyle,
} from './LibraryCardStyles'
import { sortedLabels } from '../../../lib/labelsSort'
@@ -52,12 +52,8 @@ export function ProgressBar(props: ProgressBarProps): JSX.Element {
)
}
-// Component
export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false)
- const [menuOpen, setMenuOpen] = useState(false)
-
- const originText = siteName(props.item.originalArticleUrl, props.item.url)
return (
-
-
+ ) : (
+
-
-
-
- setMenuOpen(open)}
- actionHandler={props.handleAction}
- triggerElement={
-
- }
- />
-
-
-
-
- {props.item.title}
-
- {props.item.description}
-
- {props.item.author}
- {props.item.author && originText && ' | '}
-
- {originText}
-
-
-
-
-
-
-
-
- {sortedLabels(props.item.labels).map(
- ({ name, color }, index) => (
-
- )
- )}
-
-
- {props.item.image && (
- {
- ;(e.target as HTMLElement).style.display = 'none'
- }}
- />
- )}
-
-
-
-
-
+
+
+
+ )}
)
}
+
+const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => {
+ const [menuOpen, setMenuOpen] = useState(false)
+ const originText = siteName(props.item.originalArticleUrl, props.item.url)
+
+ const handleCheckChanged = useCallback(() => {
+ props.setIsChecked(props.item.id, !props.isChecked)
+ }, [props.isChecked])
+
+ return (
+ <>
+
+
+ {props.inMultiSelect ? (
+
+
+
+ ) : (
+
+ setMenuOpen(open)}
+ actionHandler={props.handleAction}
+ triggerElement={
+
+ }
+ />
+
+ )}
+
+
+
+ {props.item.title}
+
+ {props.item.description}
+
+ {props.item.author}
+ {props.item.author && originText && ' | '}
+ {originText}
+
+
+
+
+
+
+
+ {sortedLabels(props.item.labels).map(({ name, color }, index) => (
+
+ ))}
+
+
+ {props.item.image && (
+ {
+ ;(e.target as HTMLElement).style.display = 'none'
+ }}
+ />
+ )}
+
+
+
+ >
+ )
+}
diff --git a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx
index 4571449be..2ee000a48 100644
--- a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx
+++ b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx
@@ -1,26 +1,24 @@
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { LabelChip } from '../../elements/LabelChip'
import type { LinkedItemCardProps } from './CardTypes'
-import { useMemo, useState } from 'react'
+import { useCallback, useState } from 'react'
import { DotsThree } from 'phosphor-react'
import Link from 'next/link'
import { CardMenu } from '../CardMenu'
import {
AuthorInfoStyle,
+ CardCheckbox,
LibraryItemMetadata,
MenuStyle,
MetaStyle,
siteName,
- timeAgo,
TitleStyle,
} from './LibraryCardStyles'
import { sortedLabels } from '../../../lib/labelsSort'
+import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu'
export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false)
- const [menuOpen, setMenuOpen] = useState(false)
-
- const originText = siteName(props.item.originalArticleUrl, props.item.url)
return (
-
-
+ ) : (
+
-
-
-
- setMenuOpen(open)}
- actionHandler={props.handleAction}
- triggerElement={
-
- }
- />
-
-
-
- {props.item.title}
-
- {props.item.author}
- {props.item.author && originText && ' | '}
-
- {originText}
-
-
-
-
-
- {sortedLabels(props.item.labels).map(
- ({ name, color }, index) => (
-
- )
- )}
-
-
-
-
-
+
+
+
+ )}
)
}
+
+export function LibraryListCardContent(
+ props: LinkedItemCardProps
+): JSX.Element {
+ const [menuOpen, setMenuOpen] = useState(false)
+ const originText = siteName(props.item.originalArticleUrl, props.item.url)
+
+ const handleCheckChanged = useCallback(() => {
+ props.setIsChecked(props.item.id, !props.isChecked)
+ }, [props.isChecked])
+
+ return (
+ <>
+
+
+ {props.inMultiSelect ? (
+
+
+
+ ) : (
+
+ setMenuOpen(open)}
+ actionHandler={props.handleAction}
+ triggerElement={
+
+ }
+ />
+
+ )}
+
+
+ {props.item.title}
+
+ {props.item.author}
+ {props.item.author && originText && ' | '}
+ {originText}
+
+
+
+
+ {sortedLabels(props.item.labels).map(({ name, color }, index) => (
+
+ ))}
+
+
+
+ >
+ )
+}
diff --git a/packages/web/components/templates/PrimaryDropdown.tsx b/packages/web/components/templates/PrimaryDropdown.tsx
index 5bddf16d4..f7c3acd56 100644
--- a/packages/web/components/templates/PrimaryDropdown.tsx
+++ b/packages/web/components/templates/PrimaryDropdown.tsx
@@ -23,6 +23,9 @@ type PrimaryDropdownProps = {
layout?: LayoutType
updateLayout?: (layout: LayoutType) => void
+
+ showAddLinkModal?: () => void
+ startSelectMultiple?: () => void
}
export type HeaderDropdownAction =
@@ -153,6 +156,27 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
onSelect={() => headerDropdownActionHandler('navigate-to-labels')}
title="Labels"
/>
+ {props.showAddLinkModal && (
+ <>
+
+
+ props.showAddLinkModal && props.showAddLinkModal()}
+ title="Add Link"
+ />
+ >
+ )}
+ {props.startSelectMultiple && (
+ <>
+
+ props.startSelectMultiple && props.startSelectMultiple()
+ }
+ title="Select Multiple"
+ />
+
+ >
+ )}
headerDropdownActionHandler('navigate-to-api')}
title="API Keys"
diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx
index e2be6936b..6a0ee38f3 100644
--- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx
+++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx
@@ -1,4 +1,3 @@
-import axios from 'axios'
import { Action, createAction, useKBar, useRegisterActions } from 'kbar'
import debounce from 'lodash/debounce'
import { useRouter } from 'next/router'
@@ -41,8 +40,11 @@ import { EditLibraryItemModal } from './EditItemModals'
import { EmptyLibrary } from './EmptyLibrary'
import { HighlightItemsLayout } from './HighlightsLayout'
import { LibraryFilterMenu } from './LibraryFilterMenu'
-import { LibraryHeader } from './LibraryHeader'
+import { LibraryHeader, MultiSelectMode } from './LibraryHeader'
import { UploadModal } from '../UploadModal'
+import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
+import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation'
+import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
export type LibraryMode = 'reads' | 'highlights'
@@ -117,6 +119,7 @@ export function HomeFeedContainer(): JSX.Element {
} else {
setMode('reads')
}
+ setMultiSelectMode('off')
}, [queryInputs])
useEffect(() => {
@@ -302,13 +305,21 @@ export function HomeFeedContainer(): JSX.Element {
}
break
case 'archive':
- performActionOnItem('archive', item)
+ if (multiSelectMode !== 'off') {
+ performMultiSelectAction(BulkAction.ARCHIVE)
+ } else {
+ performActionOnItem('archive', item)
+ }
break
case 'unarchive':
performActionOnItem('unarchive', item)
break
case 'delete':
- performActionOnItem('delete', item)
+ if (multiSelectMode !== 'off') {
+ performMultiSelectAction(BulkAction.DELETE)
+ } else {
+ performActionOnItem('delete', item)
+ }
break
case 'mark-read':
performActionOnItem('mark-read', item)
@@ -331,6 +342,23 @@ export function HomeFeedContainer(): JSX.Element {
return labelsTarget || linkToEdit || linkToRemove || linkToUnsubscribe
}, [labelsTarget, linkToEdit, linkToRemove, linkToUnsubscribe])
+ const [checkedItems, setCheckedItems] = useState([])
+ const [multiSelectMode, setMultiSelectMode] = useState('off')
+
+ const selectActiveArticle = useCallback(() => {
+ console.log('selecting article: ', activeItem)
+ if (activeItem) {
+ if (multiSelectMode === 'off') {
+ console.log('setting ')
+ setMultiSelectMode('some')
+ }
+ const itemId = activeItem.node.id
+ const isChecked = itemIsChecked(itemId)
+ console.log('setting is checked: ', isChecked, itemId)
+ setIsChecked(itemId, !isChecked)
+ }
+ }, [activeItem, multiSelectMode, checkedItems])
+
useKeyboardShortcuts(
libraryListCommands((action) => {
const columnCount = (container: HTMLDivElement) => {
@@ -348,7 +376,16 @@ export function HomeFeedContainer(): JSX.Element {
switch (action) {
case 'openArticle':
- handleCardAction('showDetail', activeItem)
+ 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)
@@ -435,6 +472,14 @@ export function HomeFeedContainer(): JSX.Element {
case 'sortAscending':
setQueryInputs({ ...queryInputs, sortDescending: false })
break
+ case 'beginMultiSelect':
+ if (multiSelectMode == 'off') {
+ setMultiSelectMode('none')
+ }
+ break
+ case 'endMultiSelect':
+ setMultiSelectMode('off')
+ break
}
})
)
@@ -522,11 +567,105 @@ export function HomeFeedContainer(): JSX.Element {
)
useFetchMore(handleFetchMore)
+ 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])
+ }
+ },
+ [checkedItems]
+ )
+
+ useEffect(() => {
+ switch (multiSelectMode) {
+ case 'off':
+ case 'none':
+ setCheckedItems([])
+ break
+ case 'some':
+ break
+ case 'search':
+ case 'visible':
+ const allIds = (
+ itemsPages?.flatMap((ad) => {
+ return ad.search.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) => {
+ if (multiSelectMode === 'off') {
+ return
+ }
+ if (multiSelectMode !== 'search' && checkedItems.length < 1) {
+ return
+ }
+ console.log(
+ 'performing bulk action: ',
+ action,
+ 'mode',
+ multiSelectMode,
+ checkedItems
+ )
+ ;(async () => {
+ const query =
+ multiSelectMode === 'search'
+ ? queryInputs.searchQuery || 'in:inbox'
+ : `includes:${checkedItems.join(',')}`
+ const expectedCount =
+ multiSelectMode === 'search'
+ ? itemsPages?.[0].search.pageInfo.totalCount || 0
+ : checkedItems.length
+
+ try {
+ const res = await bulkActionMutation(action, query, expectedCount)
+ if (res) {
+ switch (action) {
+ case BulkAction.ARCHIVE:
+ showSuccessToast('Items archived')
+ break
+ case BulkAction.DELETE:
+ showSuccessToast('Items deleted')
+ break
+ }
+ } else {
+ showErrorToast('Error performing bulk action')
+ }
+ } catch (err) {
+ showErrorToast('Error performing bulk action')
+ }
+ mutate()
+ })()
+ setMultiSelectMode('off')
+ },
+ [itemsPages, multiSelectMode, checkedItems]
+ )
+
return (
)
}
@@ -610,6 +754,14 @@ type HomeFeedContentProps = {
action: LinkedItemCardAction,
item: LibraryItem | undefined
) => Promise
+
+ multiSelectMode: MultiSelectMode
+ setIsChecked: (itemId: string, set: boolean) => void
+ itemIsChecked: (itemId: string) => boolean
+ setMultiSelectMode: (mode: MultiSelectMode) => void
+ numItemsSelected: number
+
+ performMultiSelectAction: (action: BulkAction) => void
}
function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
@@ -641,11 +793,17 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
updateLayout={updateLayout}
searchTerm={props.searchTerm}
applySearchQuery={(searchQuery: string) => {
- console.log('searching with searchQuery: ', searchQuery)
props.applySearchQuery(searchQuery)
}}
+ allowSelectMultiple={props.mode !== 'highlights'}
+ alwaysShowHeader={props.mode == 'highlights'}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
+ multiSelectMode={props.multiSelectMode}
+ setMultiSelectMode={props.setMultiSelectMode}
+ numItemsSelected={props.numItemsSelected}
+ showAddLinkModal={() => props.setShowAddLinkModal(true)}
+ performMultiSelectAction={props.performMultiSelectAction}
/>
)}
@@ -686,6 +846,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
type LibraryItemsLayoutProps = {
layout: LayoutType
viewer?: UserBasicData
+ inMultiSelect: boolean
+
+ isChecked: (itemId: string) => boolean
+ setIsChecked: (itemId: string, set: boolean) => void
} & HomeFeedContentProps
function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
@@ -745,6 +909,8 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
items={props.items}
layout={props.layout}
viewer={props.viewer}
+ isChecked={props.isChecked}
+ setIsChecked={props.setIsChecked}
gridContainerRef={props.gridContainerRef}
setShowEditTitleModal={props.setShowEditTitleModal}
setLinkToEdit={props.setLinkToEdit}
@@ -753,6 +919,7 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
setLinkToUnsubscribe={props.setLinkToUnsubscribe}
setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation}
actionHandler={props.actionHandler}
+ inMultiSelect={props.inMultiSelect}
/>
)}
{}}
// eslint-disable-next-line @typescript-eslint/no-empty-function
handleAction={() => {}}
/>
@@ -873,6 +1044,10 @@ type LibraryItemsProps = {
setLinkToUnsubscribe: (set: LibraryItem | undefined) => void
setShowRemoveLinkConfirmation: (show: true) => void
+ inMultiSelect: boolean
+ isChecked: (itemId: string) => boolean
+ setIsChecked: (itemId: string, set: boolean) => void
+
actionHandler: (
action: LinkedItemCardAction,
item: LibraryItem | undefined
@@ -888,12 +1063,12 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
display: 'grid',
width: '100%',
gridAutoRows: 'auto',
- borderRadius: '8px',
+ borderRadius: '5px',
gridGap: props.layout == 'LIST_LAYOUT' ? '0' : '20px',
- marginTop: props.layout == 'LIST_LAYOUT' ? '21px' : '0',
+ marginTop: '10px',
marginBottom: '0px',
- paddingTop: props.layout == 'LIST_LAYOUT' ? '0' : '21px',
- paddingBottom: props.layout == 'LIST_LAYOUT' ? '0px' : '21px',
+ paddingTop: '0',
+ paddingBottom: '0px',
overflow: 'hidden',
'@xlgDown': {
border: 'unset',
@@ -956,6 +1131,9 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
layout={props.layout}
item={linkedItem.node}
viewer={props.viewer}
+ isChecked={props.isChecked(linkedItem.node.id)}
+ setIsChecked={props.setIsChecked}
+ inMultiSelect={props.inMultiSelect}
handleAction={(action: LinkedItemCardAction) => {
if (action === 'delete') {
props.setShowRemoveLinkConfirmation(true)
diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx
index 93e08471e..3cc715600 100644
--- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx
+++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx
@@ -10,7 +10,7 @@ import { Label } from '../../../lib/networking/fragments/labelFragment'
import { theme } from '../../tokens/stitches.config'
import { currentThemeName } from '../../../lib/themeUpdater'
import { useRegisterActions } from 'kbar'
-import { HEADER_HEIGHT } from './HeaderSpacer'
+import { LogoBox } from '../../elements/LogoBox'
export const LIBRARY_LEFT_MENU_WIDTH = '233px'
@@ -30,12 +30,11 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
+
+
+
+
-
- props.setShowAddLinkModal(true)}
- />
{/* This spacer pushes library content to the right of
@@ -236,7 +243,7 @@ function MenuPanel(props: MenuPanelProps): JSX.Element {
fontWeight: '600',
fontSize: '16px',
lineHeight: '125%',
- color: '$thTextContrast',
+ color: '$thLibraryMenuPrimary',
pl: '10px',
my: '20px',
}}
@@ -324,11 +331,14 @@ function FilterButton(props: FilterButtonProps): JSX.Element {
width: '100%',
maxWidth: '100%',
height: '32px',
- backgroundColor: selected ? '#FFEA9F' : 'unset',
+
+ backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
fontSize: '14px',
fontWeight: 'regular',
fontFamily: '$display',
- color: selected ? '#3D3D3D' : '$thTextSubtle',
+ color: selected
+ ? '$thLibraryMenuSecondary'
+ : '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
@@ -337,10 +347,14 @@ function FilterButton(props: FilterButtonProps): JSX.Element {
whiteSpace: 'nowrap',
alignItems: 'center',
'&:hover': {
- backgroundColor: selected ? '#FFEA9F' : '$thBackground4',
+ backgroundColor: selected
+ ? '$thLibrarySelectionColor'
+ : '$thBackground4',
},
'&:active': {
- backgroundColor: selected ? '#FFEA9F' : '$thBackground4',
+ backgroundColor: selected
+ ? '$thLibrarySelectionColor'
+ : '$thBackground4',
},
}}
onClick={(e) => {
@@ -434,66 +448,6 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
)
}
-type AddLinkButtonProps = {
- showAddLinkModal: () => void
-}
-
-function AddLinkButton(props: AddLinkButtonProps): JSX.Element {
- const currentTheme = currentThemeName()
- const isDark = currentTheme == 'Dark'
-
- return (
- <>
-
-
-
- >
- )
-}
-
type ViewAllButtonProps = {
state: boolean
setState: (state: boolean) => void
diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx
index 0bb757279..56e7e9bd2 100644
--- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx
+++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx
@@ -1,18 +1,37 @@
-import { useRef, useState } from 'react'
-import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { theme } from '../../tokens/stitches.config'
import { FormInput } from '../../elements/FormElements'
import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { Button, IconButton } from '../../elements/Button'
-import { FunnelSimple, MagnifyingGlass, X } from 'phosphor-react'
-import { ListSelectorIcon } from '../../elements/images/ListSelectorIcon'
-import { GridSelectorIcon } from '../../elements/images/GridSelectorIcon'
+import {
+ ArchiveBox,
+ CaretDown,
+ FunnelSimple,
+ ListBullets,
+ MagnifyingGlass,
+ Prohibit,
+ SquaresFour,
+ TagSimple,
+ TrashSimple,
+ X,
+} from 'phosphor-react'
import { LayoutType } from './HomeFeedContainer'
import { PrimaryDropdown } from '../PrimaryDropdown'
-import { LogoBox } from '../../elements/LogoBox'
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
import { HeaderSpacer, HEADER_HEIGHT } from './HeaderSpacer'
+import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu'
+import {
+ ScrollOffsetChangeset,
+ useScrollWatcher,
+} from '../../../lib/hooks/useScrollWatcher'
+import { CardCheckbox } from '../../patterns/LibraryCards/LibraryCardStyles'
+import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
+import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
+import { ConfirmationModal } from '../../patterns/ConfirmationModal'
+
+export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search'
type LibraryHeaderProps = {
layout: LayoutType
@@ -21,11 +40,38 @@ type LibraryHeaderProps = {
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
+ alwaysShowHeader: boolean
+ allowSelectMultiple: boolean
+
showFilterMenu: boolean
setShowFilterMenu: (show: boolean) => void
+
+ showAddLinkModal: () => void
+
+ numItemsSelected: number
+ multiSelectMode: MultiSelectMode
+ setMultiSelectMode: (mode: MultiSelectMode) => void
+
+ performMultiSelectAction: (action: BulkAction) => void
}
export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
+ const [showBackground, setShowBackground] = useState(false)
+
+ useEffect(() => {
+ if (!props.alwaysShowHeader) {
+ setShowBackground(window.scrollY > 5 || props.multiSelectMode !== 'off')
+ } else {
+ setShowBackground(true)
+ }
+ }, [props.multiSelectMode, props.alwaysShowHeader])
+
+ useScrollWatcher((changeset: ScrollOffsetChangeset) => {
+ if (!props.alwaysShowHeader) {
+ setShowBackground(window.scrollY > 5 || props.multiSelectMode !== 'off')
+ }
+ }, 0)
+
return (
<>
{/* These will display/hide depending on breakpoints */}
@@ -59,7 +107,7 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
return (
-
-
)
@@ -109,11 +163,19 @@ function SmallHeaderLayout(props: LibraryHeaderProps): JSX.Element {
) : (
<>
-
+ {props.multiSelectMode === 'off' && }
>
)}
@@ -177,6 +239,10 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
const [focused, setFocused] = useState(false)
const [searchTerm, setSearchTerm] = useState(props.searchTerm ?? '')
+ useEffect(() => {
+ setSearchTerm(props.searchTerm ?? '')
+ }, [props.searchTerm])
+
const border = props.compact
? focused
? '1px solid $omnivoreCtaYellow'
@@ -203,7 +269,7 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
height: '38px',
width: '100%',
maxWidth: '521px',
- bg: props.compact ? 'white' : '$thBackground2',
+ bg: '$thLibrarySearchbox',
borderRadius: '6px',
border: border,
}}
@@ -317,55 +383,257 @@ type ControlButtonBoxProps = {
layout: LayoutType
updateLayout: (layout: LayoutType) => void
setShowInlineSearch?: (show: boolean) => void
+
+ showAddLinkModal: () => void
+
+ allowSelectMultiple: boolean
+
+ numItemsSelected: number
+ multiSelectMode: MultiSelectMode
+ setMultiSelectMode: (mode: MultiSelectMode) => void
+
+ performMultiSelectAction: (action: BulkAction) => void
+
+ searchTerm: string | undefined
+ applySearchQuery: (searchQuery: string) => void
+}
+
+function MultiSelectControlButtonBox(
+ props: ControlButtonBoxProps
+): JSX.Element {
+ const [showConfirmDelete, setShowConfirmDelete] = useState(false)
+
+ return (
+
+
+ {/* */}
+
+
+ {showConfirmDelete && (
+ {
+ props.performMultiSelectAction(BulkAction.DELETE)
+ }}
+ onOpenChange={(open: boolean) => {
+ setShowConfirmDelete(false)
+ }}
+ />
+ )}
+
+ )
+}
+
+type SearchControlButtonBoxProps = ControlButtonBoxProps
+
+function SearchControlButtonBox(
+ props: SearchControlButtonBoxProps
+): JSX.Element {
+ return (
+ <>
+
+
+ {
+ props.setMultiSelectMode('none')
+ }
+ : undefined
+ }
+ showAddLinkModal={props.showAddLinkModal}
+ />
+ >
+ )
}
function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element {
+ const [isChecked, setIsChecked] = useState(false)
+
+ useEffect(() => {
+ if (props.multiSelectMode === 'off' || props.multiSelectMode === 'none') {
+ setIsChecked(false)
+ }
+ }, [props.multiSelectMode])
+
return (
<>
-
-
-
-
+ {props.multiSelectMode !== 'off' && (
+
+ {
+ const newValue = !isChecked
+ props.setMultiSelectMode(newValue ? 'visible' : 'none')
+ setIsChecked(newValue)
+ }}
+ />
+
+
+ }
+ >
+ {
+ setIsChecked(true)
+ props.setMultiSelectMode('visible')
+ }}
+ title="All"
+ />
+ {
+ setIsChecked(true)
+ props.setMultiSelectMode('search')
+ }}
+ title="All matching search"
+ />
+
+
+
+ {props.numItemsSelected}{' '}
+
+ selected
+
+
+
+ )}
+ {props.multiSelectMode !== 'off' ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
- {props.setShowInlineSearch && (
+ {props.setShowInlineSearch && props.multiSelectMode === 'off' && (
{
+ props.setMultiSelectMode('none')
+ }}
/>
)}
diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts
index 1aa21caca..d37381370 100644
--- a/packages/web/components/tokens/stitches.config.ts
+++ b/packages/web/components/tokens/stitches.config.ts
@@ -163,7 +163,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
//utility
overlay: 'rgba(63, 62, 60, 0.2)',
- // New theme, special naming to keep things straigh
+ // New theme, special naming to keep things straight
// once all switch over, we will rename
thBackground: '#FFFFFF',
thBackground2: '#F3F3F3',
@@ -172,6 +172,13 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
thBackground5: '#F5F5F5',
thBackgroundActive: '#F9F9F9',
thBackgroundContrast: '#FFFFFF',
+ thLeftMenuBackground: '#FCFCFC',
+ thLibraryBackground: '#F3F3F3',
+ thLibrarySearchbox: '#FCFCFC',
+ thLibraryMenuPrimary: '#3D3D3D',
+ thLibraryMenuSecondary: '#3D3D3D',
+ thLibraryMenuUnselected: '#898989',
+ thLibrarySelectionColor: '#FFEA9F',
thTextContrast: '#1E1E1E',
thTextContrast2: '#3D3D3D',
@@ -259,6 +266,13 @@ const darkThemeSpec = {
thBackground5: '#3D3D3D',
thBackgroundActive: '#2E2E2E',
thBackgroundContrast: '#000000',
+ thLeftMenuBackground: '#1D1D1D',
+ thLibraryBackground: '#333333',
+ thLibrarySearchbox: '#3D3D3D',
+ thLibraryMenuPrimary: '#EBEBEB',
+ thLibraryMenuSecondary: '#EBEBEB',
+ thLibraryMenuUnselected: '#898989',
+ thLibrarySelectionColor: '#3D3D3D',
thTextContrast: '#FFFFFF',
thTextContrast2: '#EBEBEB',
diff --git a/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts b/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts
index 2c4bd0bab..9b216f21b 100644
--- a/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts
+++ b/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts
@@ -74,6 +74,7 @@ export function primaryCommands(
type LibraryListKeyboardAction =
| 'openArticle'
+ | 'selectArticle'
| 'openOriginalArticle'
| 'moveFocusToNextListItem'
| 'moveFocusToPreviousListItem'
@@ -88,6 +89,8 @@ type LibraryListKeyboardAction =
| 'shareItem'
| 'showAddLinkModal'
| 'showEditLabelsModal'
+ | 'beginMultiSelect'
+ | 'endMultiSelect'
export function libraryListCommands(
actionHandler: (action: LibraryListKeyboardAction) => void
@@ -99,6 +102,14 @@ export function libraryListCommands(
shortcutKeyDescription: 'enter/return',
callback: () => actionHandler('openArticle'),
},
+ {
+ shortcutKeys: ['x'],
+ actionDescription: 'Select article',
+ shortcutKeyDescription: 'x',
+ callback: () => {
+ actionHandler('selectArticle')
+ },
+ },
{
shortcutKeys: ['o'],
actionDescription: 'Open original article',
@@ -123,6 +134,18 @@ export function libraryListCommands(
shortcutKeyDescription: 'k or left arrow',
callback: () => actionHandler('moveFocusToPreviousListItem'),
},
+ {
+ shortcutKeys: ['m', 's'],
+ actionDescription: 'Begin multi select',
+ shortcutKeyDescription: 'm then s',
+ callback: () => actionHandler('beginMultiSelect'),
+ },
+ {
+ shortcutKeys: ['escape'],
+ actionDescription: 'End multi select',
+ shortcutKeyDescription: 'Escape',
+ callback: () => actionHandler('endMultiSelect'),
+ },
// {
// shortcutKeys: ['e'],
// actionDescription: 'Archive item',
diff --git a/packages/web/lib/keyboardShortcuts/useKeyboardShortcuts.ts b/packages/web/lib/keyboardShortcuts/useKeyboardShortcuts.ts
index 35af88f33..1ac8dfa67 100644
--- a/packages/web/lib/keyboardShortcuts/useKeyboardShortcuts.ts
+++ b/packages/web/lib/keyboardShortcuts/useKeyboardShortcuts.ts
@@ -109,7 +109,6 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => {
const { target } = keydownEvent
if (!keydownEvent.key) return
const key = keydownEvent.key.toLowerCase()
-
if (keys[key] === undefined) return
if (keys[key] === false) {
if (key === 'k' && metaPressed()) {
diff --git a/packages/web/lib/networking/mutations/bulkActionMutation.ts b/packages/web/lib/networking/mutations/bulkActionMutation.ts
index 935e5a8e6..630645174 100644
--- a/packages/web/lib/networking/mutations/bulkActionMutation.ts
+++ b/packages/web/lib/networking/mutations/bulkActionMutation.ts
@@ -4,6 +4,7 @@ import { gqlFetcher } from '../networkHelpers'
export enum BulkAction {
ARCHIVE = 'ARCHIVE',
DELETE = 'DELETE',
+ ADD_LABELS = 'ADD_LABELS',
}
type BulkActionResponseData = {
@@ -15,24 +16,40 @@ type BulkActionResponse = {
bulkAction?: BulkActionResponseData
}
-export async function bulkActionMutation(action: BulkAction): Promise {
+export async function bulkActionMutation(
+ action: BulkAction,
+ query: string,
+ expectedCount: number
+): Promise {
const mutation = gql`
- mutation {
- bulkAction (action: ${action}) {
- ... on BulkActionSuccess {
- success
- }
- ... on BulkActionError {
- errorCodes
+ mutation BulkAction(
+ $action: BulkActionType!
+ $query: String!
+ $expectedCount: Int
+ ) {
+ bulkAction(
+ query: $query
+ action: $action
+ expectedCount: $expectedCount
+ ) {
+ ... on BulkActionSuccess {
+ success
+ }
+ ... on BulkActionError {
+ errorCodes
+ }
}
}
- }
`
console.log('bulkActionbulkActionMutation', mutation)
try {
- const response = await gqlFetcher(mutation, { action })
+ const response = await gqlFetcher(mutation, {
+ action,
+ query,
+ expectedCount,
+ })
console.log('response', response)
const data = response as BulkActionResponse | undefined
return data?.bulkAction?.success ?? false
diff --git a/packages/web/pages/home.tsx b/packages/web/pages/home.tsx
index 714abbeaa..612d28d97 100644
--- a/packages/web/pages/home.tsx
+++ b/packages/web/pages/home.tsx
@@ -18,7 +18,7 @@ function LoadedContent(): JSX.Element {
diff --git a/packages/web/pages/tools/bulk.tsx b/packages/web/pages/tools/bulk.tsx
deleted file mode 100644
index 97bea8282..000000000
--- a/packages/web/pages/tools/bulk.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import { useCallback, useState } from 'react'
-import { applyStoredTheme } from '../../lib/themeUpdater'
-
-import { VStack } from '../../components/elements/LayoutPrimitives'
-
-import { StyledText } from '../../components/elements/StyledText'
-import { ProfileLayout } from '../../components/templates/ProfileLayout'
-import {
- BulkAction,
- bulkActionMutation,
-} from '../../lib/networking/mutations/bulkActionMutation'
-import { Button } from '../../components/elements/Button'
-import { theme } from '../../components/tokens/stitches.config'
-import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
-import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
-import { useRouter } from 'next/router'
-
-type RunningState = 'none' | 'confirming' | 'running' | 'completed'
-
-export default function BulkPerformer(): JSX.Element {
- const router = useRouter()
-
- applyStoredTheme(false)
-
- const [action, setAction] = useState()
- const [errorMessage, setErrorMessage] = useState()
- const [runningState, setRunningState] = useState('none')
-
- const performAction = useCallback(() => {
- ;(async () => {
- console.log('performing action: ', action)
- if (!action) {
- showErrorToast('Unable to run action, no action set.')
- return
- }
- try {
- const success = await bulkActionMutation(action)
- if (!success) {
- throw 'Success not returned'
- }
- showSuccessToast('Bulk action is being performed.')
- setRunningState('completed')
- } catch (err) {
- showErrorToast('Error performing bulk action.')
- }
- })()
- }, [action])
-
- return (
-
-
-
- Perform a Bulk Action
-
-
- Use this tool to perform a bulk operation on all the items in your
- library.
- More info
-
-
- Note: This operation can not be undone.
-
-
- {runningState == 'completed' ? (
-
- Your bulk action has started. Please note that it can take some
- time for these actions to complete. During this time, we recommend
- not modifying your library as new items could be updated by the
- action.
-
- ) : (
- <>
-
-
-
-
-
- >
- )}
-
- {runningState == 'confirming' && (
- setRunningState('none')}
- />
- )}
- {runningState == 'completed' && (
-
-
-
- )}
-
- {errorMessage && (
- {errorMessage}
- )}
-
-
-
- )
-}