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 24, 2024
1 parent 629c806 commit 6c8467f
Show file tree
Hide file tree
Showing 15 changed files with 161 additions and 99 deletions.
5 changes: 5 additions & 0 deletions .changeset/popular-guests-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/design-system': minor
---

Table row data now supports an isSelected prop.
5 changes: 5 additions & 0 deletions .changeset/stupid-planets-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

The keys table now has pagination controls.
4 changes: 2 additions & 2 deletions apps/renterd-e2e/src/specs/keys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ test('batch delete multiple keys', async ({ page }) => {
const rowIdx3 = getKeyRowByIndex(page, 3)

// Select all 4 keys.
await rowIdx0.getByLabel('select key').click()
await rowIdx3.getByLabel('select key').click({ modifiers: ['Shift'] })
await rowIdx0.click()
await rowIdx3.click({ modifiers: ['Shift'] })

// Delete all 4 keys.
const menu = page.getByLabel('key multiselect menu')
Expand Down
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
12 changes: 2 additions & 10 deletions apps/renterd/components/Keys/KeysBatchMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,10 @@ import { useKeys } from '../../../contexts/keys'
import { KeysBatchDelete } from './KeysBatchDelete'

export function KeysBatchMenu() {
const { selectionCount, isPageAllSelected, pageCount, deselectAll } =
useKeys()
const { multiSelect } = useKeys()

return (
<MultiSelectionMenu
isVisible={selectionCount > 0}
selectionCount={selectionCount}
isPageAllSelected={isPageAllSelected}
deselectAll={deselectAll}
pageCount={pageCount}
entityWord="key"
>
<MultiSelectionMenu multiSelect={multiSelect} entityWord="key">
<KeysBatchDelete />
</MultiSelectionMenu>
)
Expand Down
18 changes: 18 additions & 0 deletions apps/renterd/components/Keys/KeysStatsMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PaginatorKnownTotal } from '@siafoundation/design-system'
import { useKeys } from '../../../contexts/keys'

export function KeysStatsMenu() {
const { limit, offset, datasetCount, pageCount, dataState } = useKeys()
return (
<div className="flex w-full">
<div className="flex-1" />
<PaginatorKnownTotal
offset={offset}
limit={limit}
datasetTotal={datasetCount}
pageTotal={pageCount}
isLoading={dataState === 'loading'}
/>
</div>
)
}
2 changes: 2 additions & 0 deletions apps/renterd/components/Keys/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { KeysActionsMenu } from './KeysActionsMenu'
import { StateError } from './StateError'
import { useKeys } from '../../contexts/keys'
import { KeysBatchMenu } from './KeysBatchMenu'
import { KeysStatsMenu } from './KeysStatsMenu'

export function Keys() {
const { openDialog } = useDialog()
Expand All @@ -31,6 +32,7 @@ export function Keys() {
sidenav={<RenterdSidenav />}
openSettings={() => openDialog('settings')}
actions={<KeysActionsMenu />}
stats={<KeysStatsMenu />}
>
<div className="p-6 min-w-fit">
<KeysBatchMenu />
Expand Down
21 changes: 6 additions & 15 deletions apps/renterd/contexts/keys/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Checkbox,
ControlGroup,
TableColumn,
ValueCopyable,
} from '@siafoundation/design-system'
Expand All @@ -19,21 +18,13 @@ export const columns: KeysTableColumn[] = [
fixed: true,
contentClassName: '!pl-3 !pr-4',
cellClassName: 'w-[20px] !pl-0 !pr-0',
heading: ({ context: { onSelectPage, isPageAllSelected } }) => (
<ControlGroup className="flex h-4">
<Checkbox onClick={onSelectPage} checked={isPageAllSelected} />
</ControlGroup>
),
render: ({ data: { id, key }, context: { selectionMap, onSelect } }) => (
<ControlGroup className="flex h-4">
<Checkbox
aria-label="select key"
onClick={(e) => onSelect(id, e)}
checked={!!selectionMap[id]}
/>
<KeyContextMenu s3Key={key} />
</ControlGroup>
heading: ({ context: { multiSelect } }) => (
<Checkbox
onClick={multiSelect.onSelectPage}
checked={multiSelect.isPageAllSelected}
/>
),
render: ({ data: { key } }) => <KeyContextMenu s3Key={key} />,
},
{
id: 'key',
Expand Down
44 changes: 21 additions & 23 deletions apps/renterd/contexts/keys/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,29 @@ function useKeysMain() {
sortDirection,
})

const datasetPage = useMemo<KeyData[] | undefined>(() => {
const _datasetPage = useMemo<KeyData[] | undefined>(() => {
if (!datasetFiltered) {
return undefined
}
return datasetFiltered.slice(offset, offset + limit)
}, [datasetFiltered, offset, limit])

const multiSelect = useMultiSelect(_datasetPage)

const datasetPage = useMemo(() => {
if (!_datasetPage) {
return undefined
}
return _datasetPage.map((datum) => {
return {
...datum,
onClick: (e: React.MouseEvent<HTMLTableRowElement>) =>
multiSelect.onSelect(datum.id, e),
isSelected: !!multiSelect.selectionMap[datum.id],
}
})
}, [_datasetPage, multiSelect])

const filteredTableColumns = useMemo(
() =>
columns.filter(
Expand All @@ -95,31 +111,18 @@ function useKeysMain() {
)

const dataState = useDatasetEmptyState(
datasetFiltered,
datasetPage,
response.isValidating,
response.error,
filters
)

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

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

return {
Expand All @@ -132,12 +135,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
41 changes: 23 additions & 18 deletions libs/design-system/src/multi/MultiSelectionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,20 @@ 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,
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 = multiSelect.selectionCount > 0
return (
<div className="z-20 fixed bottom-5 left-0 right-0 flex justify-center dark pointer-events-none">
<AnimatePresence>
Expand All @@ -40,20 +34,31 @@ export function MultiSelectionMenu({
aria-label={entityWord + ' multiselect menu'}
className="pl-3 pr-2 py-2 min-w-[250px] flex gap-2 items-center rounded-lg light:bg-black pointer-events-auto"
>
{!!selectionCount && (
{!!multiSelect.selectionCount && (
<Text size="14">
{pluralize(selectionCount, entityWord, {
{`${pluralize(multiSelect.selectionCount, entityWord, {
plural: entityWordPlural,
})}{' '}
selected
})} selected${
multiSelect.someSelectedItemsOutsideCurrentPage &&
multiSelect.someSelectedOnCurrentPage
? ' on this and other pages'
: !multiSelect.someSelectedItemsOutsideCurrentPage &&
multiSelect.someSelectedOnCurrentPage
? ''
: multiSelect.someSelectedItemsOutsideCurrentPage &&
!multiSelect.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">
<Button
tip="Deselect all"
onClick={multiSelect.deselectAll}
size="small"
>
<Close16 />
</Button>
</Panel>
Expand Down
Loading

0 comments on commit 6c8467f

Please sign in to comment.