From 97f422cf3f957071cc64df91afe68698f4ce577f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 3 Aug 2023 13:21:46 +0800 Subject: [PATCH] Improve multiselect --- .../web/components/elements/ProgressBar.tsx | 4 +- .../patterns/LibraryCards/CardTypes.tsx | 4 +- .../LibraryCards/LibraryCardStyles.tsx | 37 +-- .../patterns/LibraryCards/LibraryGridCard.tsx | 188 ++++++------ .../LibraryCards/LibraryHighlightGridCard.tsx | 1 - .../LibraryCards/LibraryHoverActions.tsx | 2 - .../patterns/LibraryCards/LibraryListCard.tsx | 267 +++++++++++++----- .../components/templates/PrimaryLayout.tsx | 1 + .../web/components/templates/UploadModal.tsx | 2 - .../components/templates/article/Article.tsx | 1 - .../templates/article/HighlightsLayer.tsx | 4 - .../components/templates/article/Notebook.tsx | 1 - .../templates/homeFeed/HomeFeedContainer.tsx | 44 +-- .../templates/homeFeed/LibraryHeader.tsx | 114 +++++--- packages/web/next.config.js | 2 +- 15 files changed, 421 insertions(+), 251 deletions(-) diff --git a/packages/web/components/elements/ProgressBar.tsx b/packages/web/components/elements/ProgressBar.tsx index cc39172a5..a367bcd51 100644 --- a/packages/web/components/elements/ProgressBar.tsx +++ b/packages/web/components/elements/ProgressBar.tsx @@ -11,9 +11,8 @@ export function ProgressBar(props: ProgressBarProps): JSX.Element { return ( diff --git a/packages/web/components/patterns/LibraryCards/CardTypes.tsx b/packages/web/components/patterns/LibraryCards/CardTypes.tsx index 2c9077661..799509cb2 100644 --- a/packages/web/components/patterns/LibraryCards/CardTypes.tsx +++ b/packages/web/components/patterns/LibraryCards/CardTypes.tsx @@ -1,6 +1,7 @@ import { LayoutType } from '../../templates/homeFeed/HomeFeedContainer' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' import type { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { MultiSelectMode } from '../../templates/homeFeed/LibraryHeader' export type LinkedItemCardAction = | 'showDetail' @@ -23,9 +24,10 @@ export type LinkedItemCardProps = { handleAction: (action: LinkedItemCardAction) => void - inMultiSelect: boolean isChecked: boolean setIsChecked: (itemId: string, set: boolean) => void + multiSelectMode: MultiSelectMode + isHovered?: boolean } diff --git a/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx b/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx index cf421f402..19fdf4e4a 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx @@ -1,8 +1,8 @@ import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' -import { useMemo } from 'react' +import { ChangeEvent, useMemo } from 'react' import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery' -import { Box, SpanBox } from '../../elements/LayoutPrimitives' +import { Box, SpanBox, VStack } from '../../elements/LayoutPrimitives' dayjs.extend(relativeTime) @@ -25,7 +25,7 @@ export const MenuStyle = { export const MetaStyle = { width: '100%', color: '$thTextSubtle3', - fontSize: '13px', + fontSize: '12px', fontWeight: '400', fontFamily: '$display', } @@ -34,7 +34,7 @@ export const TitleStyle = { color: '$thTextContrast2', fontSize: '16px', fontWeight: '700', - lineHeight: '1.25', + lineHeight: '1', fontFamily: '$display', overflow: 'hidden', textOverflow: 'ellipsis', @@ -67,11 +67,11 @@ export const AuthorInfoStyle = { whiteSpace: 'nowrap', maxWidth: '240px', overflow: 'hidden', - height: '21px', color: '$thTextSubtle3', fontSize: '12px', fontWeight: '400', fontFamily: '$display', + lineHeight: '1', } export const timeAgo = (date: string | undefined): string => { @@ -137,14 +137,6 @@ export function LibraryItemMetadata( {highlightCount > 0 ? ` • ${highlightCount} highlight${highlightCount > 1 ? 's' : ''}` : null} - {(props.showProgress && props.item.readingProgressPercent) ?? 0 > 0 ? ( - <> - {` | `} - - {`${Math.round(props.item.readingProgressPercent)}%`} - - - ) : null} ) } @@ -156,10 +148,19 @@ type CardCheckBoxProps = { export function CardCheckbox(props: CardCheckBoxProps): JSX.Element { return ( - +
{ + event.stopPropagation() + }} + > + { + props.handleChanged() + }} + > +
) } diff --git a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx index b87b96e82..9ea58c51f 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx @@ -4,7 +4,7 @@ import type { LinkedItemCardProps } from './CardTypes' import { CoverImage } from '../../elements/CoverImage' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' -import { useCallback, useState } from 'react' +import { ChangeEvent, useCallback, useState } from 'react' import Link from 'next/link' import { AuthorInfoStyle, @@ -29,6 +29,8 @@ import { import { CardMenu } from '../CardMenu' import { DotsThree } from 'phosphor-react' import { isTouchScreenDevice } from '../../../lib/deviceType' +import { ProgressBarOverlay } from './LibraryListCard' +import { FallbackImage } from './FallbackImage' dayjs.extend(relativeTime) @@ -115,38 +117,32 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element { setIsHovered(false) }} > - {props.inMultiSelect ? ( - - ) : ( - <> - {!isTouchScreenDevice() && ( - - - - )} - - - - - - + {!isTouchScreenDevice() && ( + + + )} + + + + + ) } @@ -189,25 +185,43 @@ const Fallback = (props: FallbackProps): JSX.Element => { type GridImageProps = { src?: string title?: string + readingProgress?: number } const GridImage = (props: GridImageProps): JSX.Element => { const [displayFallback, setDisplayFallback] = useState(props.src == undefined) - return displayFallback ? ( - - ) : ( - { - setDisplayFallback(true) - }} - /> + return ( + <> + {(props.readingProgress ?? 0) > 0 && ( + + )} + {displayFallback ? ( + + ) : ( + { + setDisplayFallback(true) + }} + /> + )} + ) } @@ -217,45 +231,55 @@ const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => { const originText = siteName(props.item.originalArticleUrl, props.item.url) const handleCheckChanged = useCallback(() => { - setIsChecked(item.id, !isChecked) - }, [setIsChecked, isChecked]) + const newValue = !isChecked + setIsChecked(item.id, newValue) + }, [setIsChecked, isChecked, props]) return ( - - {props.inMultiSelect ? ( - - - - ) : ( - - setMenuOpen(open)} - actionHandler={props.handleAction} - triggerElement={ - - } - /> - - )} + + + + + + setMenuOpen(open)} + actionHandler={props.handleAction} + triggerElement={ + + } + /> + { { const [menuOpen, setMenuOpen] = useState(false) - console.log(' props.isHovered || menuOpen', props.isHovered, menuOpen) - return ( - {props.inMultiSelect ? ( - - ) : ( - <> - {!isTouchScreenDevice() && ( - - - - )} - - - - - - + {!isTouchScreenDevice() && ( + + + )} + + + + + ) } +type ProgressBarOverlayProps = { + top: number + width: string + value: number + bottomRadius: string +} + +export const ProgressBarOverlay = ( + props: ProgressBarOverlayProps +): JSX.Element => { + return ( + + + + ) +} + +type ListImageProps = { + src?: string + title?: string + readingProgress?: number +} + +const ListImage = (props: ListImageProps): JSX.Element => { + const [displayFallback, setDisplayFallback] = useState(props.src == undefined) + + return ( + <> + {(props.readingProgress ?? 0) > 0 && ( + + )} + {displayFallback ? ( + + ) : ( + { + setDisplayFallback(true) + }} + /> + )} + + ) +} + export function LibraryListCardContent( props: LinkedItemCardProps ): JSX.Element { @@ -140,57 +216,96 @@ export function LibraryListCardContent( }, [isChecked, setIsChecked, item]) return ( - <> - - - {props.inMultiSelect ? ( - - - - ) : ( - - setMenuOpen(open)} - actionHandler={props.handleAction} - triggerElement={ - - } - /> - - )} - + + input': { + p: '0px', + m: '0px', + }, + }} + > + + + + + + + setMenuOpen(open)} + actionHandler={props.handleAction} + triggerElement={ + + } + /> + + - - {props.item.title} - - - {props.item.author} - {props.item.author && originText && ' | '} - {originText} - + + + + + {props.item.title} + + {props.item.author} + {props.item.author && originText && ' | '} + + {originText} + + + - + ) } diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index 74bd10dbd..c7368b2ae 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -11,6 +11,7 @@ import { logoutMutation } from '../../lib/networking/mutations/logoutMutation' import { setupAnalytics } from '../../lib/analytics' import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts' import { applyStoredTheme } from '../../lib/themeUpdater' +import { theme } from '../tokens/stitches.config' type PrimaryLayoutProps = { children: ReactNode diff --git a/packages/web/components/templates/UploadModal.tsx b/packages/web/components/templates/UploadModal.tsx index 8fac6c53e..021210fa3 100644 --- a/packages/web/components/templates/UploadModal.tsx +++ b/packages/web/components/templates/UploadModal.tsx @@ -130,7 +130,6 @@ export function UploadModal(props: UploadModalProps): JSX.Element { Papa.parse(file.file, { step: function (row, parser) { - console.log('row: ', row) if (Array.isArray(row.data)) { try { if (row.data[0].trim().length < 1) { @@ -211,7 +210,6 @@ export function UploadModal(props: UploadModalProps): JSX.Element { ;(async () => { for (const file of addedFiles) { try { - console.log('using content type: ', file.file.type) const uploadInfo = await uploadSignedUrlForFile(file) if (!uploadInfo.uploadSignedUrl) { showErrorToast('No upload URL available') diff --git a/packages/web/components/templates/article/Article.tsx b/packages/web/components/templates/article/Article.tsx index 2b9c44da8..2ad51d320 100644 --- a/packages/web/components/templates/article/Article.tsx +++ b/packages/web/components/templates/article/Article.tsx @@ -192,7 +192,6 @@ export function Article(props: ArticleProps): JSX.Element { const img = element as HTMLImageElement const width = Number(img.getAttribute('data-omnivore-width')) const height = Number(img.getAttribute('data-omnivore-height')) - console.log('width and height: ', width, height) if (!isNaN(width) && !isNaN(height) && width < 100 && height < 100) { img.style.setProperty('width', `${width}px`) diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx index 032b0838e..f095918f3 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -297,7 +297,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { const { target, pageX, pageY } = event if (!target || (target as Node)?.nodeType !== Node.ELEMENT_NODE) { - console.log(' -- returning early from page tap') return } @@ -375,7 +374,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { highlightIdAttribute ) const highlight = highlights.find(($0) => $0.id === id) - console.log('double tapped highlight: ', highlight) setFocusedHighlight(highlight) openNoteModal({ @@ -387,8 +385,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { highlightNoteIdAttribute ) const highlight = highlights.find(($0) => $0.id === id) - console.log('double tapped highlight with note: ', highlight) - setFocusedHighlight(highlight) openNoteModal({ diff --git a/packages/web/components/templates/article/Notebook.tsx b/packages/web/components/templates/article/Notebook.tsx index 671b342ee..a4c1c0011 100644 --- a/packages/web/components/templates/article/Notebook.tsx +++ b/packages/web/components/templates/article/Notebook.tsx @@ -91,7 +91,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element { ) const createNote = useCallback((text: string) => { - console.log('creating note: ', newNoteId, noteState.current.isCreating) noteState.current.isCreating = true noteState.current.createStarted = new Date() ;(async () => { diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 8c886fc91..57cba74b5 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -84,11 +84,13 @@ export function HomeFeedContainer(): JSX.Element { const gridContainerRef = useRef(null) - const [labelsTarget, setLabelsTarget] = - useState(undefined) + const [labelsTarget, setLabelsTarget] = useState( + undefined + ) - const [notebookTarget, setNotebookTarget] = - useState(undefined) + const [notebookTarget, setNotebookTarget] = useState( + undefined + ) const [showAddLinkModal, setShowAddLinkModal] = useState(false) const [showEditTitleModal, setShowEditTitleModal] = useState(false) @@ -372,15 +374,12 @@ export function HomeFeedContainer(): JSX.Element { 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]) @@ -493,7 +492,6 @@ export function HomeFeedContainer(): JSX.Element { handleCardAction('set-labels', activeItem) break case 'openNotebook': - console.log('openNotebook: ', notebookTarget) handleCardAction('open-notebook', activeItem) break case 'sortDescending': @@ -553,7 +551,6 @@ export function HomeFeedContainer(): JSX.Element { name: 'Mark item as read', shortcut: ['m', 'r'], perform: () => { - console.log('mark read action') handleCardAction('mark-read', activeItem) }, }), @@ -612,8 +609,16 @@ export function HomeFeedContainer(): JSX.Element { checkedItems.splice(checkedItems.indexOf(itemId), 1) setCheckedItems([...checkedItems]) } + + if (set && multiSelectMode == 'off') { + setMultiSelectMode('some') + } + + if (checkedItems.length < 1) { + setMultiSelectMode('off') + } }, - [checkedItems] + [checkedItems, multiSelectMode, setMultiSelectMode] ) useEffect(() => { @@ -806,10 +811,12 @@ type HomeFeedContentProps = { item: LibraryItem | undefined ) => Promise - multiSelectMode: MultiSelectMode setIsChecked: (itemId: string, set: boolean) => void itemIsChecked: (itemId: string) => boolean + + multiSelectMode: MultiSelectMode setMultiSelectMode: (mode: MultiSelectMode) => void + numItemsSelected: number performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void @@ -861,7 +868,6 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { setShowAddLinkModal={props.setShowAddLinkModal} searchTerm={props.searchTerm} applySearchQuery={(searchQuery: string) => { - console.log('searching with searchQuery: ', searchQuery) props.applySearchQuery(searchQuery) }} showFilterMenu={showFilterMenu} @@ -880,7 +886,6 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { @@ -897,7 +902,9 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { type LibraryItemsLayoutProps = { layout: LayoutType viewer?: UserBasicData - inMultiSelect: boolean + + multiSelectMode: MultiSelectMode + setMultiSelectMode: (mode: MultiSelectMode) => void isChecked: (itemId: string) => boolean setIsChecked: (itemId: string, set: boolean) => void @@ -970,7 +977,7 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { setLinkToUnsubscribe={props.setLinkToUnsubscribe} setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation} actionHandler={props.actionHandler} - inMultiSelect={props.inMultiSelect} + multiSelectMode={props.multiSelectMode} /> )} {}} @@ -1095,9 +1103,9 @@ type LibraryItemsProps = { setLinkToUnsubscribe: (set: LibraryItem | undefined) => void setShowRemoveLinkConfirmation: (show: true) => void - inMultiSelect: boolean isChecked: (itemId: string) => boolean setIsChecked: (itemId: string, set: boolean) => void + multiSelectMode: MultiSelectMode actionHandler: ( action: LinkedItemCardAction, @@ -1186,7 +1194,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element { viewer={props.viewer} isChecked={props.isChecked(linkedItem.node.id)} setIsChecked={props.setIsChecked} - inMultiSelect={props.inMultiSelect} + multiSelectMode={props.multiSelectMode} handleAction={(action: LinkedItemCardAction) => { if (action === 'delete') { props.setShowRemoveLinkConfirmation(true) diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx index a77d468cf..cb823203b 100644 --- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -28,6 +28,7 @@ import { TrashIcon } from '../../elements/icons/TrashIcon' import { LabelIcon } from '../../elements/icons/LabelIcon' import { ListViewIcon } from '../../elements/icons/ListViewIcon' import { GridViewIcon } from '../../elements/icons/GridViewIcon' +import { CaretDownIcon } from '../../elements/icons/CaretDownIcon' export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search' @@ -377,9 +378,7 @@ type ControlButtonBoxProps = { applySearchQuery: (searchQuery: string) => void } -function MultiSelectControlButtonBox( - props: ControlButtonBoxProps -): JSX.Element { +function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element { const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [showLabelsModal, setShowLabelsModal] = useState(false) @@ -498,7 +497,7 @@ function SearchControlButtonBox( ) } -function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element { +const MuliSelectControl = (props: ControlButtonBoxProps): JSX.Element => { const [isChecked, setIsChecked] = useState(false) useEffect(() => { @@ -507,6 +506,69 @@ function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element { } }, [props.multiSelectMode]) + return ( + + + { + const newValue = !isChecked + props.setMultiSelectMode(newValue ? 'visible' : 'off') + setIsChecked(newValue) + }} + /> + + + } + > + { + setIsChecked(true) + props.setMultiSelectMode('visible') + }} + title="All" + /> + {/* { + setIsChecked(true) + props.setMultiSelectMode('search') + }} + title="All matching search" + /> */} + + + + + ) +} + +function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element { 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 @@ -597,7 +629,7 @@ function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element { )} {props.multiSelectMode !== 'off' ? ( <> - + ) : ( diff --git a/packages/web/next.config.js b/packages/web/next.config.js index e6ebad8fa..9477bdcc6 100644 --- a/packages/web/next.config.js +++ b/packages/web/next.config.js @@ -6,7 +6,7 @@ const ContentSecurityPolicy = ` font-src 'self' data: cdn.jsdelivr.net https://js.intercomcdn.com https://fonts.intercomcdn.com; form-action 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://getpocket.com/auth/authorize https://intercom.help https://api-iam.intercom.io https://api-iam.eu.intercom.io https://api-iam.au.intercom.io; frame-ancestors 'none'; - frame-src self accounts.google.com platform.twitter.com www.youtube.com www.youtube-nocookie.com; + frame-src 'self' accounts.google.com platform.twitter.com www.youtube.com www.youtube-nocookie.com; manifest-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' accounts.google.com widget.intercom.io js.intercomcdn.com platform.twitter.com cdnjs.cloudflare.com cdn.jsdelivr.net cdn.segment.com; style-src 'self' 'unsafe-inline' accounts.google.com cdnjs.cloudflare.com;