Add support for multi select labels

This commit is contained in:
Jackson Harper 2023-06-21 13:55:20 +08:00
parent 2acdd683df
commit 2293cc0256
7 changed files with 77 additions and 48 deletions

View file

@ -125,7 +125,7 @@ export const Button = styled('button', {
fontWeight: '600',
fontFamily: '$inter',
cursor: 'pointer',
border: '1px solid $thBorderSubtle',
border: '1px solid $thBorderColor',
bg: 'transparent',
'&:hover': {
border: '1px solid $grayBorderHover',

View file

@ -18,8 +18,6 @@ export interface LabelsProvider {
}
type SetLabelsControlProps = {
provider: LabelsProvider
inputValue: string
setInputValue: (value: string) => void
clearInputState: () => void
@ -39,6 +37,8 @@ type SetLabelsControlProps = {
selectOrCreateLabel: (value: string) => void
errorMessage?: string
footer?: React.ReactNode
}
type HeaderProps = SetLabelsControlProps & {
@ -273,7 +273,6 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
newSelectedLabels = [...props.selectedLabels, label]
}
props.dispatchLabels({ type: 'SAVE', labels: newSelectedLabels })
props.provider.labels = newSelectedLabels
props.clearInputState()
revalidate()
@ -383,7 +382,6 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
}}
>
<Header
provider={props.provider}
focused={focusedIndex === undefined}
resetFocusedIndex={() => setFocusedIndex(undefined)}
inputValue={inputValue}
@ -443,10 +441,14 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
/>
))}
</VStack>
<Footer
filterText={inputValue}
focused={focusedIndex === filteredLabels.length + 1}
/>
{props.footer ? (
props.footer
) : (
<Footer
filterText={inputValue}
focused={focusedIndex === filteredLabels.length + 1}
/>
)}
</VStack>
)
}

View file

@ -192,7 +192,6 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
<ModalTitleBar title="Labels" onOpenChange={props.onOpenChange} />
</SpanBox>
<SetLabelsControl
provider={props.provider}
inputValue={inputValue}
setInputValue={setInputValue}
clearInputState={clearInputState}

View file

@ -1,7 +1,14 @@
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 {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react'
import { Toaster } from 'react-hot-toast'
import TopBarProgress from 'react-topbar-progress-indicator'
import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll'
@ -45,6 +52,11 @@ import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter'
import { NotebookPresenter } from '../article/NotebookPresenter'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import {
LabelAction,
LabelsDispatcher,
} from '../../../lib/hooks/useSetPageLabels'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
export type LibraryMode = 'reads' | 'highlights'
@ -624,20 +636,13 @@ export function HomeFeedContainer(): JSX.Element {
)
const performMultiSelectAction = useCallback(
(action: BulkAction) => {
(action: BulkAction, labelIds?: string[]) => {
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'
@ -649,13 +654,21 @@ export function HomeFeedContainer(): JSX.Element {
: checkedItems.length
try {
const res = await bulkActionMutation(action, query, expectedCount)
const res = await bulkActionMutation(
action,
query,
expectedCount,
labelIds
)
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
@ -791,7 +804,7 @@ type HomeFeedContentProps = {
setMultiSelectMode: (mode: MultiSelectMode) => void
numItemsSelected: number
performMultiSelectAction: (action: BulkAction) => void
performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void
}
function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {

View file

@ -13,6 +13,7 @@ import {
MagnifyingGlass,
Prohibit,
SquaresFour,
Tag,
TrashSimple,
X,
} from 'phosphor-react'
@ -29,6 +30,9 @@ import { CardCheckbox } from '../../patterns/LibraryCards/LibraryCardStyles'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { AddBulkLabelsModal } from '../article/AddBulkLabelsModal'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search'
@ -51,26 +55,10 @@ type LibraryHeaderProps = {
multiSelectMode: MultiSelectMode
setMultiSelectMode: (mode: MultiSelectMode) => void
performMultiSelectAction: (action: BulkAction) => void
performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => 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 (
<>
<VStack
@ -122,6 +110,7 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
multiSelectMode={props.multiSelectMode}
setMultiSelectMode={props.setMultiSelectMode}
showAddLinkModal={props.showAddLinkModal}
bulkSetLabels={props.bulkSetLabels}
performMultiSelectAction={props.performMultiSelectAction}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
@ -171,6 +160,7 @@ function SmallHeaderLayout(props: LibraryHeaderProps): JSX.Element {
multiSelectMode={props.multiSelectMode}
setMultiSelectMode={props.setMultiSelectMode}
showAddLinkModal={props.showAddLinkModal}
bulkSetLabels={props.bulkSetLabels}
performMultiSelectAction={props.performMultiSelectAction}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
@ -388,7 +378,7 @@ type ControlButtonBoxProps = {
multiSelectMode: MultiSelectMode
setMultiSelectMode: (mode: MultiSelectMode) => void
performMultiSelectAction: (action: BulkAction) => void
performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
@ -398,6 +388,7 @@ function MultiSelectControlButtonBox(
props: ControlButtonBoxProps
): JSX.Element {
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showLabelsModal, setShowLabelsModal] = useState(false)
return (
<HStack alignment="center" distribution="center" css={{ gap: '20px' }}>
@ -415,6 +406,20 @@ function MultiSelectControlButtonBox(
/>
<SpanBox css={{ '@lgDown': { display: 'none' } }}>Archive</SpanBox>
</Button>
<Button
style="outline"
onClick={(e) => {
setShowLabelsModal(true)
e.preventDefault()
}}
>
<Tag
width={20}
height={20}
color={theme.colors.thTextContrast2.toString()}
/>
<SpanBox css={{ '@lgDown': { display: 'none' } }}>Add Labels</SpanBox>
</Button>
<Button
style="outline"
onClick={(e) => {
@ -441,7 +446,7 @@ function MultiSelectControlButtonBox(
height={20}
color={theme.colors.thTextContrast2.toString()}
/>
Cancel
<SpanBox css={{ '@lgDown': { display: 'none' } }}>Cancel</SpanBox>
</Button>
{showConfirmDelete && (
<ConfirmationModal
@ -455,6 +460,17 @@ function MultiSelectControlButtonBox(
}}
/>
)}
{showLabelsModal && (
<AddBulkLabelsModal
bulkSetLabels={(labels: Label[]) => {
const labelIds = labels.map((l) => l.id)
props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds)
}}
onOpenChange={(open: boolean) => {
setShowLabelsModal(false)
}}
/>
)}
</HStack>
)
}
@ -564,7 +580,7 @@ function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element {
triggerElement={
<CaretDown
size={15}
color={theme.colors.thBorderSubtle.toString()}
color={theme.colors.graySolid.toString()}
weight="fill"
/>
}

View file

@ -2,12 +2,7 @@ import { useReducer } from 'react'
import { Label } from '../networking/fragments/labelFragment'
import { showErrorToast } from '../toastHelpers'
import { setLabelsForHighlight } from '../networking/mutations/setLabelsForHighlight'
export type LabelAction = 'RESET' | 'TEMP' | 'SAVE'
export type LabelsDispatcher = (action: {
type: LabelAction
labels: Label[]
}) => void
import { LabelsDispatcher } from './useSetPageLabels'
export const useSetHighlightLabels = (
highlightId?: string

View file

@ -19,17 +19,20 @@ type BulkActionResponse = {
export async function bulkActionMutation(
action: BulkAction,
query: string,
expectedCount: number
expectedCount: number,
labelIds?: string[]
): Promise<boolean> {
const mutation = gql`
mutation BulkAction(
$action: BulkActionType!
$query: String!
$expectedCount: Int
$labelIds: [ID!]
) {
bulkAction(
query: $query
action: $action
labelIds: $labelIds
expectedCount: $expectedCount
) {
... on BulkActionSuccess {
@ -48,6 +51,7 @@ export async function bulkActionMutation(
const response = await gqlFetcher(mutation, {
action,
query,
labelIds,
expectedCount,
})
console.log('response', response)