Skip to content

Commit

Permalink
refactor: refine multiselect API
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Oct 18, 2024
1 parent 6c7e368 commit 1a9941b
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 72 deletions.
16 changes: 6 additions & 10 deletions apps/renterd/components/Keys/KeysBatchMenu/KeysBatchDelete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,11 @@ import { useDialog } from '../../../contexts/dialog'
import { useKeys } from '../../../contexts/keys'

export function KeysBatchDelete() {
const { selectionMap, deselect } = useKeys()
const { multiSelect } = useKeys()

const ids = useMemo(
() => Object.entries(selectionMap).map(([_, item]) => item.id),
[selectionMap]
)
const keys = useMemo(
() => Object.entries(selectionMap).map(([_, item]) => item.key),
[selectionMap]
() => Object.entries(multiSelect.selectionMap).map(([_, item]) => item.key),
[multiSelect.selectionMap]
)
const { openConfirmDialog } = useDialog()
const settingsS3 = useSettingsS3()
Expand All @@ -43,13 +39,13 @@ export function KeysBatchDelete() {
},
},
})
deselect(ids)
multiSelect.deselectAll()
if (response.error) {
triggerErrorToast({ title: 'Error deleting keys', body: response.error })
} else {
triggerSuccessToast({ title: `Keys deleted` })
}
}, [settingsS3.data, settingsS3Update, deselect, keys, ids])
}, [settingsS3.data, settingsS3Update, multiSelect, keys])

return (
<Button
Expand All @@ -64,7 +60,7 @@ export function KeysBatchDelete() {
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to delete the{' '}
{ids.length.toLocaleString()} selected keys?
{multiSelect.selectionCount.toLocaleString()} selected keys?
</Paragraph>
</div>
),
Expand Down
13 changes: 8 additions & 5 deletions apps/renterd/contexts/keys/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ export const columns: KeysTableColumn[] = [
fixed: true,
contentClassName: '!pl-3 !pr-4',
cellClassName: 'w-[20px] !pl-0 !pr-0',
heading: ({ context: { onSelectPage, isPageAllSelected } }) => (
heading: ({ context: { multiSelect } }) => (
<ControlGroup className="flex h-4">
<Checkbox onClick={onSelectPage} checked={isPageAllSelected} />
<Checkbox
onClick={multiSelect.onSelectPage}
checked={multiSelect.isPageAllSelected}
/>
</ControlGroup>
),
render: ({ data: { id, key }, context: { selectionMap, onSelect } }) => (
render: ({ data: { id, key }, context: { multiSelect } }) => (
<ControlGroup className="flex h-4">
<Checkbox
aria-label="select key"
onClick={(e) => onSelect(id, e)}
checked={!!selectionMap[id]}
onClick={(e) => multiSelect.onSelect(id, e)}
checked={!!multiSelect.selectionMap[id]}
/>
<KeyContextMenu s3Key={key} />
</ControlGroup>
Expand Down
24 changes: 4 additions & 20 deletions apps/renterd/contexts/keys/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,25 +101,14 @@ function useKeysMain() {
filters
)

const {
onSelect,
deselect,
deselectAll,
selectionMap,
selectionCount,
onSelectPage,
isPageAllSelected,
} = useMultiSelect(dataset)
const multiSelect = useMultiSelect(dataset)

const cellContext = useMemo(
() =>
({
selectionMap,
onSelect,
onSelectPage,
isPageAllSelected,
multiSelect,
} as CellContext),
[selectionMap, onSelect, onSelectPage, isPageAllSelected]
[multiSelect]
)

return {
Expand All @@ -132,12 +121,7 @@ function useKeysMain() {
datasetCount: dataset?.length || 0,
datasetFilteredCount: datasetFiltered?.length || 0,
columns: filteredTableColumns,
selectionMap,
selectionCount,
onSelectPage,
isPageAllSelected,
deselect,
deselectAll,
multiSelect,
cellContext,
dataset,
datasetPage,
Expand Down
7 changes: 2 additions & 5 deletions apps/renterd/contexts/keys/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MouseEvent } from 'react'
import { MultiSelect } from '@siafoundation/design-system'

export type KeyData = {
id: string
Expand All @@ -7,10 +7,7 @@ export type KeyData = {
}

export type CellContext = {
selectionMap: Record<string, KeyData>
onSelect: (id: string, e: MouseEvent<HTMLButtonElement>) => void
onSelectPage: () => void
isPageAllSelected: boolean | 'indeterminate'
multiSelect: MultiSelect
}

export type TableColumnId = 'selection' | 'actions' | 'key' | 'secret'
Expand Down
13 changes: 10 additions & 3 deletions libs/design-system/src/components/Table/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { CSSProperties, forwardRef, useMemo } from 'react'
import { CSSProperties, forwardRef, MouseEvent, useMemo } from 'react'
import { cx } from 'class-variance-authority'
import { useDroppable, useDraggable } from '@dnd-kit/core'
import {
Expand All @@ -13,7 +13,8 @@ type Data = {
isDraggable?: boolean
isDroppable?: boolean
className?: string
onClick?: () => void
onClick?: (e: MouseEvent<HTMLTableRowElement>) => void
isSelected?: boolean
}

export type Row<Data, Context> = {
Expand Down Expand Up @@ -91,7 +92,13 @@ export function createTableRow<
data-testid={data.id}
onClick={data.onClick}
className={cx(
'border-b border-gray-200/50 dark:border-graydark-100',
'border-b',
data.isSelected
? [
'bg-blue-400 border-blue-500/30',
'dark:bg-blue-600/50 dark:border-blue-600/20',
]
: 'border-gray-200/50 dark:border-graydark-100',
data.onClick ? 'cursor-pointer' : '',
data.className,
className
Expand Down
4 changes: 2 additions & 2 deletions libs/design-system/src/components/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Tooltip } from '../../core/Tooltip'
import { Panel } from '../../core/Panel'
import { Text } from '../../core/Text'
import { useCallback, useMemo } from 'react'
import { MouseEvent, useCallback, useMemo } from 'react'
import { cx } from 'class-variance-authority'
import { ChevronDown16, ChevronUp16 } from '@siafoundation/react-icons'
import { times } from '@technically/lodash'
Expand All @@ -30,7 +30,7 @@ type Data = {
id: string
isDraggable?: boolean
isDroppable?: boolean
onClick?: () => void
onClick?: (e: MouseEvent<HTMLTableRowElement>) => void
}

export type Row<Data, Context> = {
Expand Down
4 changes: 3 additions & 1 deletion libs/design-system/src/core/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ export function Tooltip({
panelStyles()
)}
>
{typeof content === 'string' || Array.isArray(content) ? (
{typeof content === 'string' ||
(React.isValidElement(content) &&
content?.type === React.Fragment) ? (
<Paragraph size="12">{content}</Paragraph>
) : (
content
Expand Down
38 changes: 22 additions & 16 deletions libs/design-system/src/multi/MultiSelectionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,25 @@ import { Panel } from '../core/Panel'
import { Text } from '../core/Text'
import { pluralize } from '@siafoundation/units'
import { Close16 } from '@siafoundation/react-icons'
import { MultiSelect } from './useMultiSelect'

export function MultiSelectionMenu({
isVisible,
selectionCount,
isPageAllSelected,
deselectAll,
pageCount,
multiSelect: {
selectionCount,
deselectAll,
someSelectedItemsOutsideCurrentPage,
someSelectedOnCurrentPage,
},
children,
entityWord,
entityWordPlural,
}: {
isVisible: boolean
selectionCount: number
isPageAllSelected: boolean | 'indeterminate'
pageCount: number
multiSelect: MultiSelect
children: React.ReactNode
deselectAll: () => void
entityWord: string
entityWordPlural?: string
}) {
const isVisible = selectionCount > 0
return (
<div className="z-20 fixed bottom-5 left-0 right-0 flex justify-center dark pointer-events-none">
<AnimatePresence>
Expand All @@ -42,15 +41,22 @@ export function MultiSelectionMenu({
>
{!!selectionCount && (
<Text size="14">
{pluralize(selectionCount, entityWord, {
{`${pluralize(selectionCount, entityWord, {
plural: entityWordPlural,
})}{' '}
selected
})} selected${
someSelectedItemsOutsideCurrentPage &&
someSelectedOnCurrentPage
? ' on this and other pages'
: !someSelectedItemsOutsideCurrentPage &&
someSelectedOnCurrentPage
? ''
: someSelectedItemsOutsideCurrentPage &&
!someSelectedOnCurrentPage
? ' on other pages'
: ''
}`}
</Text>
)}
{isPageAllSelected && selectionCount > pageCount && (
<Text>across multiple pages</Text>
)}
<div className="flex-1" />
{children}
<Button tip="Deselect all" onClick={deselectAll} size="small">
Expand Down
64 changes: 54 additions & 10 deletions libs/design-system/src/multi/useMultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { MouseEvent, useCallback, useMemo, useState } from 'react'

export type MultiSelect = ReturnType<typeof useMultiSelect>

export function useMultiSelect<Item extends { id: string }>(dataset?: Item[]) {
const [selectionMap, setSelectionMap] = useState<Record<string, Item>>({})
const [, setLastSelectedItem] = useState<{
Expand All @@ -10,7 +12,7 @@ export function useMultiSelect<Item extends { id: string }>(dataset?: Item[]) {
}>()

const onSelect = useCallback(
(id: string, e: MouseEvent<HTMLButtonElement>) => {
(id: string, e: MouseEvent<HTMLButtonElement | HTMLDivElement>) => {
if (!dataset) {
return
}
Expand Down Expand Up @@ -61,6 +63,31 @@ export function useMultiSelect<Item extends { id: string }>(dataset?: Item[]) {
return getIsPageAllSelected({ dataset, selectionMap })
}, [dataset, selectionMap])

const selectedIds = useMemo(
() =>
Object.entries(selectionMap)
.filter(([_, item]) => !!item)
.map(([id]) => id),
[selectionMap]
)

const someSelectedItemsOutsideCurrentPage = useMemo(() => {
if (!dataset) {
if (selectedIds.length === 0) {
return false
}
return true
}
return selectedIds.some((id) => !dataset.some((datum) => datum.id === id))
}, [dataset, selectedIds])

const someSelectedOnCurrentPage = useMemo(() => {
if (!dataset) {
return false
}
return dataset.some((datum) => selectionMap[datum.id])
}, [dataset, selectionMap])

const onSelectPage = useCallback(() => {
if (!dataset) {
return
Expand Down Expand Up @@ -112,15 +139,32 @@ export function useMultiSelect<Item extends { id: string }>(dataset?: Item[]) {
[selectionMap]
)

return {
onSelect,
onSelectPage,
selectionMap,
isPageAllSelected,
selectionCount,
deselect,
deselectAll,
}
return useMemo(
() => ({
onSelect,
onSelectPage,
selectionMap,
selectedIds,
isPageAllSelected,
selectionCount,
someSelectedItemsOutsideCurrentPage,
someSelectedOnCurrentPage,
deselect,
deselectAll,
}),
[
onSelect,
onSelectPage,
selectionMap,
selectedIds,
isPageAllSelected,
selectionCount,
someSelectedItemsOutsideCurrentPage,
someSelectedOnCurrentPage,
deselect,
deselectAll,
]
)
}

function getIsPageAllSelected<Item extends { id: string }>({
Expand Down

0 comments on commit 1a9941b

Please sign in to comment.