mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add support for multi select labels
This commit is contained in:
parent
2acdd683df
commit
2293cc0256
7 changed files with 77 additions and 48 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue