diff --git a/.changeset/modern-bags-exercise.md b/.changeset/modern-bags-exercise.md new file mode 100644 index 000000000..01911e456 --- /dev/null +++ b/.changeset/modern-bags-exercise.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +Files can now be moved by dragging into or out of directories. diff --git a/.changeset/sour-snakes-train.md b/.changeset/sour-snakes-train.md new file mode 100644 index 000000000..3e43a7b70 --- /dev/null +++ b/.changeset/sour-snakes-train.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/design-system': minor +--- + +The Table now supports drag and drop on rows. diff --git a/apps/renterd/components/Files/FilesExplorer.tsx b/apps/renterd/components/Files/FilesExplorer.tsx index 6dcf59fd1..862633b21 100644 --- a/apps/renterd/components/Files/FilesExplorer.tsx +++ b/apps/renterd/components/Files/FilesExplorer.tsx @@ -14,6 +14,12 @@ export function FilesExplorer() { sortDirection, sortableColumns, toggleSort, + onDragEnd, + onDragOver, + onDragStart, + onDragCancel, + onDragMove, + draggingObject, } = useFiles() const canUpload = useCanUpload() return ( @@ -34,6 +40,12 @@ export function FilesExplorer() { sortDirection={sortDirection} toggleSort={toggleSort} rowSize="dense" + onDragStart={onDragStart} + onDragOver={onDragOver} + onDragEnd={onDragEnd} + onDragCancel={onDragCancel} + onDragMove={onDragMove} + draggingDatum={draggingObject} /> diff --git a/apps/renterd/contexts/files/dataset.tsx b/apps/renterd/contexts/files/dataset.tsx index 51aa2d80f..63952ab1b 100644 --- a/apps/renterd/contexts/files/dataset.tsx +++ b/apps/renterd/contexts/files/dataset.tsx @@ -24,6 +24,7 @@ import { useRouter } from 'next/router' import { useMemo } from 'react' type Props = { + setActiveDirectory: (func: (directory: string[]) => string[]) => void activeDirectoryPath: string uploadsList: ObjectData[] sortDirection: 'asc' | 'desc' @@ -34,6 +35,7 @@ type Props = { const defaultLimit = 50 export function useDataset({ + setActiveDirectory, activeDirectoryPath, uploadsList, sortDirection, @@ -107,19 +109,28 @@ export function useDataset({ size: 0, health: 0, name, + onClick: () => { + setActiveDirectory((p) => p.concat(name)) + }, type: 'bucket', } }) } else if (response.data) { response.data.entries?.forEach(({ name: key, size, health }) => { const path = bucketAndResponseKeyToFilePath(activeBucketName, key) + const name = getFilename(key) dataMap[path] = { id: path, path, bucket: activeBucket, size, health, - name: getFilename(key), + name, + onClick: isDirectory(key) + ? () => { + setActiveDirectory((p) => p.concat(name.slice(0, -1))) + } + : undefined, type: isDirectory(key) ? 'directory' : 'file', } }) diff --git a/apps/renterd/contexts/files/index.tsx b/apps/renterd/contexts/files/index.tsx index 46c743c0d..e1dfd5f1d 100644 --- a/apps/renterd/contexts/files/index.tsx +++ b/apps/renterd/contexts/files/index.tsx @@ -6,7 +6,12 @@ import { import { useRouter } from 'next/router' import { createContext, useCallback, useContext, useMemo } from 'react' import { columns } from './columns' -import { defaultSortField, columnsDefaultVisible, sortOptions } from './types' +import { + defaultSortField, + columnsDefaultVisible, + sortOptions, + ObjectData, +} from './types' import { FullPath, FullPathSegments, @@ -17,6 +22,7 @@ import { import { useUploads } from './uploads' import { useDownloads } from './downloads' import { useDataset } from './dataset' +import { useMove } from './move' function useFilesMain() { const { @@ -77,13 +83,29 @@ function useFilesMain() { const { limit, offset, response, dataset } = useDataset({ activeDirectoryPath, + setActiveDirectory, uploadsList, sortField, sortDirection, filters, }) - const datasetPage = useMemo(() => { + const { + onDragEnd, + onDragOver, + onDragCancel, + onDragMove, + onDragStart, + draggingObject, + } = useMove({ + dataset, + activeDirectory, + setActiveDirectory, + mutate: response.mutate, + }) + + // Add parent directory to the dataset + const _datasetPage = useMemo(() => { if (!dataset) { return null } @@ -94,7 +116,10 @@ function useFilesMain() { name: '..', path: '..', type: 'directory', - }, + onClick: () => { + setActiveDirectory((p) => p.slice(0, -1)) + }, + } as ObjectData, ...dataset, ] } @@ -104,6 +129,29 @@ function useFilesMain() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataset]) + // Add drag and drop properties to the dataset + const datasetPage = useMemo(() => { + if (!_datasetPage) { + return null + } + return _datasetPage.map((d) => { + if ( + draggingObject && + draggingObject.id !== d.id && + d.type === 'directory' + ) { + return { + ...d, + isDroppable: true, + } + } + return { + ...d, + isDraggable: d.type !== 'bucket' && !d.isUploading, + } + }) + }, [_datasetPage, draggingObject]) + const filteredTableColumns = useMemo( () => columns.filter( @@ -177,6 +225,12 @@ function useFilesMain() { sortDirection, resetDefaultColumnVisibility, getFileUrl, + onDragStart, + onDragEnd, + onDragMove, + onDragCancel, + onDragOver, + draggingObject, } } diff --git a/apps/renterd/contexts/files/move.tsx b/apps/renterd/contexts/files/move.tsx new file mode 100644 index 000000000..2fd8e133a --- /dev/null +++ b/apps/renterd/contexts/files/move.tsx @@ -0,0 +1,143 @@ +import { ObjectData } from './types' +import { useCallback, useState } from 'react' +import { + DragStartEvent, + DragEndEvent, + DragOverEvent, + DragMoveEvent, + DragCancelEvent, +} from '@dnd-kit/core' +import { FullPathSegments, getDirectorySegmentsFromPath } from './paths' +import { useObjectRename } from '@siafoundation/react-renterd' +import { triggerErrorToast } from '@siafoundation/design-system' +import { getRenameParams } from './rename' + +type Props = { + activeDirectory: FullPathSegments + setActiveDirectory: ( + func: (directory: FullPathSegments) => FullPathSegments + ) => void + dataset?: ObjectData[] + mutate: () => void +} + +export function useMove({ + dataset, + activeDirectory, + setActiveDirectory, + mutate, +}: Props) { + const [draggingObject, setDraggingObject] = useState(null) + const [, setNavTimeout] = useState() + const rename = useObjectRename() + + const moveFiles = useCallback( + async (e: DragEndEvent) => { + const { bucket, from, to, mode } = getRenameParams(e, activeDirectory) + if (from === to) { + return + } + const response = await rename.post({ + payload: { + force: false, + bucket, + from, + to, + mode, + }, + }) + mutate() + if (response.error) { + triggerErrorToast(response.error) + } + }, + [mutate, rename, activeDirectory] + ) + + const delayedNavigation = useCallback( + (directory?: FullPathSegments) => { + if (!directory) { + setNavTimeout((t) => { + if (t) { + clearTimeout(t) + } + return null + }) + return + } + const newTimeout = setTimeout(() => { + setActiveDirectory(() => directory) + }, 1000) + setNavTimeout((t) => { + if (t) { + clearTimeout(t) + } + return newTimeout + }) + }, + [setNavTimeout, setActiveDirectory] + ) + + const scheduleNavigation = useCallback( + (e: { collisions: { id: string | number }[] }) => { + if (!e.collisions.length) { + delayedNavigation(undefined) + } else { + const path = e.collisions?.[0].id as string + if (path === '..') { + delayedNavigation(activeDirectory.slice(0, -1)) + } else { + delayedNavigation(getDirectorySegmentsFromPath(path)) + } + } + }, + [delayedNavigation, activeDirectory] + ) + + const onDragStart = useCallback( + (e: DragStartEvent) => { + setDraggingObject(dataset.find((d) => d.id === e.active.id) || null) + }, + [dataset, setDraggingObject] + ) + + const onDragOver = useCallback( + (e: DragOverEvent) => { + scheduleNavigation(e) + }, + [scheduleNavigation] + ) + + const onDragMove = useCallback( + (e: DragMoveEvent) => { + scheduleNavigation(e) + }, + [scheduleNavigation] + ) + + const onDragEnd = useCallback( + async (e: DragEndEvent) => { + delayedNavigation(undefined) + setDraggingObject(undefined) + moveFiles(e) + }, + [setDraggingObject, delayedNavigation, moveFiles] + ) + + const onDragCancel = useCallback( + async (e: DragCancelEvent) => { + delayedNavigation(undefined) + setDraggingObject(undefined) + }, + [setDraggingObject, delayedNavigation] + ) + + return { + onDragEnd, + onDragOver, + onDragCancel, + onDragMove, + onDragStart, + draggingObject, + } +} diff --git a/apps/renterd/contexts/files/paths.ts b/apps/renterd/contexts/files/paths.ts index 05472bb90..70fc55ac8 100644 --- a/apps/renterd/contexts/files/paths.ts +++ b/apps/renterd/contexts/files/paths.ts @@ -2,6 +2,12 @@ export type FullPathSegments = string[] export type FullPath = string export type KeyPath = string +export function join(a: string, b: string): FullPath { + const _a = a.endsWith('/') ? a.slice(0, -1) : a + const _b = b.startsWith('/') ? b.slice(1) : b + return `${_a}/${_b}` +} + export function getFilePath(dirPath: FullPath, name: string): FullPath { const n = name.startsWith('/') ? name.slice(1) : name return dirPath + n @@ -24,7 +30,7 @@ export function getBucketFromPath(path: FullPath): string { return path.split('/')[0] } -function getKeyFromPath(path: FullPath): KeyPath { +export function getKeyFromPath(path: FullPath): KeyPath { const segsWithoutBucket = path.split('/').slice(1).join('/') return `/${segsWithoutBucket}` } diff --git a/apps/renterd/contexts/files/rename.spec.ts b/apps/renterd/contexts/files/rename.spec.ts new file mode 100644 index 000000000..a7d6b8e69 --- /dev/null +++ b/apps/renterd/contexts/files/rename.spec.ts @@ -0,0 +1,66 @@ +import { getRenameParams } from './rename' + +describe('rename', () => { + it('directory', () => { + expect( + getRenameParams( + { + active: { + id: 'default/path/a/', + }, + collisions: [], + }, + ['default', 'path', 'to'] + ) + ).toEqual({ + bucket: 'default', + from: '/path/a/', + to: '/path/to/a/', + mode: 'multi', + }) + }) + it('directory specific', () => { + expect( + getRenameParams( + { + active: { + id: 'default/path/a/', + }, + collisions: [ + { + id: 'default/path/nested/', + }, + ], + }, + ['default', 'path', 'to'] + ) + ).toEqual({ + bucket: 'default', + from: '/path/a/', + to: '/path/nested/a/', + mode: 'multi', + }) + }) + it('file', () => { + expect( + getRenameParams( + { + active: { + id: 'default/path/a', + }, + collisions: [ + { + id: 'default/path/nested/', + }, + ], + }, + ['default', 'path', 'to'] + ) + ).toEqual({ + bucket: 'default', + from: '/path/a', + to: '/path/nested/a', + mode: 'single', + }) + }) +}) diff --git a/apps/renterd/contexts/files/rename.ts b/apps/renterd/contexts/files/rename.ts new file mode 100644 index 000000000..6096a3c12 --- /dev/null +++ b/apps/renterd/contexts/files/rename.ts @@ -0,0 +1,35 @@ +import { + FullPathSegments, + getBucketFromPath, + getFilename, + getKeyFromPath, + pathSegmentsToPath, + join, +} from './paths' + +type Id = string | number + +export function getRenameParams( + e: { active: { id: Id }; collisions: { id: Id }[] }, + activeDirectory: FullPathSegments +) { + const fromPath = String(e.active.id) + let toPath = pathSegmentsToPath(activeDirectory) + if (e.collisions.length) { + if (e.collisions[0].id === '..') { + toPath = pathSegmentsToPath(activeDirectory.slice(0, -1)) + } else { + toPath = String(e.collisions[0].id) + } + } + const filename = getFilename(fromPath) + const bucket = getBucketFromPath(fromPath) + const from = getKeyFromPath(fromPath) + const to = getKeyFromPath(join(toPath, filename)) + return { + bucket, + from, + to, + mode: filename.endsWith('/') ? 'multi' : 'single', + } as const +} diff --git a/apps/renterd/contexts/files/types.ts b/apps/renterd/contexts/files/types.ts index 8383b9bce..e2b46665f 100644 --- a/apps/renterd/contexts/files/types.ts +++ b/apps/renterd/contexts/files/types.ts @@ -14,7 +14,10 @@ export type ObjectData = { size: number type: ObjectType isUploading?: boolean + isDraggable?: boolean + isDroppable?: boolean loaded?: number + onClick?: () => void } export type TableColumnId = diff --git a/libs/design-system/package.json b/libs/design-system/package.json index 51d33ad42..1be217eee 100644 --- a/libs/design-system/package.json +++ b/libs/design-system/package.json @@ -29,6 +29,7 @@ "@visx/react-spring": "2.18.0", "@visx/glyph": "2.17.0", "@react-spring/web": "^9.7.3", + "@dnd-kit/core": "^6.1.0", "react-idle-timer": "^5.7.2", "formik": "^2.2.9", "yup": "^0.32.11", diff --git a/libs/design-system/src/components/Table.tsx b/libs/design-system/src/components/Table.tsx deleted file mode 100644 index dcf2e87ec..000000000 --- a/libs/design-system/src/components/Table.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import { Tooltip } from '../core/Tooltip' -import { Panel } from '../core/Panel' -import { Text } from '../core/Text' -import { useCallback } from 'react' -import { cx } from 'class-variance-authority' -import { CaretDown16, CaretUp16 } from '@siafoundation/react-icons' -import { times } from '@technically/lodash' - -type Data = { - id: string - onClick?: () => void -} - -export type Row = { - data: Data - context?: Context -} - -export type TableColumn = { - id: Columns - label: string - icon?: React.ReactNode - tip?: string - size?: number | string - cellClassName?: string - contentClassName?: string - render: React.FC> - summary?: () => React.ReactNode -} - -type Props< - Columns extends string, - SortField extends string, - D extends Data, - Context -> = { - data?: D[] - context?: Context - columns: TableColumn[] - sortField?: SortField - sortDirection?: 'asc' | 'desc' - toggleSort?: (field: SortField) => void - sortableColumns?: SortField[] - summary?: boolean - rowSize?: 'dense' | 'default' - pageSize: number - isLoading: boolean - emptyState?: React.ReactNode - focusId?: string - focusColor?: 'green' | 'red' | 'amber' | 'blue' | 'default' -} - -export function Table< - Columns extends string, - SortField extends string, - D extends Data, - Context ->({ - columns, - data, - context, - sortField, - sortDirection, - sortableColumns, - toggleSort, - summary, - rowSize = 'default', - pageSize, - isLoading, - emptyState, - focusId, - focusColor = 'default', -}: Props) { - let show = 'emptyState' - - if (isLoading && !data?.length) { - show = 'skeleton' - } - - if (data?.length) { - show = 'currentData' - } - - const getCellClassNames = useCallback( - (i: number, className: string | undefined, rounded?: boolean) => - cx( - i === 0 ? 'pl-6' : 'pl-4', - i === columns.length - 1 ? 'pr-6' : 'pr-4', - rounded - ? [ - i === 0 ? 'rounded-tl-lg' : '', - i === columns.length - 1 ? 'rounded-tr-lg' : '', - ] - : '', - className - ), - [columns] - ) - - const getContentClassNames = useCallback( - (i: number, className?: string) => cx('flex items-center', className), - [] - ) - - return ( - - - - - {columns.map( - ( - { id, icon, label, tip, cellClassName, contentClassName }, - i - ) => { - const isSortable = - sortableColumns?.includes(id as unknown as SortField) && - !!toggleSort - const isSortActive = (sortField as string) === id - return ( - - ) - } - )} - - - - {summary && ( - - {columns.map( - ({ id, summary, contentClassName, cellClassName }, i) => ( - - ) - )} - - )} - {show === 'currentData' && - data?.map((row) => ( - - {columns.map( - ( - { - id, - render: Render, - contentClassName: className, - cellClassName, - }, - i - ) => ( - - ) - )} - - ))} - {show === 'skeleton' && - times(pageSize).map((i) => ( - - {columns.map(({ id, contentClassName, cellClassName }, i) => ( - - ))} - - ))} - -
-
-
{ - if (isSortable) { - toggleSort(id as unknown as SortField) - } - }} - className={cx( - getContentClassNames(i, contentClassName), - isSortable ? 'cursor-pointer' : '' - )} - > - - - {icon ?
{icon}
: null} - - {label} - -
-
- {isSortActive && ( - - {sortDirection === 'asc' ? ( - - ) : ( - - )} - - )} - {isSortable && !isSortActive && ( - - - - )} - {/* {tip && {tip}} */} -
-
-
-
- {summary && summary()} -
-
-
- -
-
-
-
- {show === 'emptyState' && emptyState} -
- ) -} diff --git a/libs/design-system/src/components/Table/TableRow.tsx b/libs/design-system/src/components/Table/TableRow.tsx new file mode 100644 index 000000000..6d3861570 --- /dev/null +++ b/libs/design-system/src/components/Table/TableRow.tsx @@ -0,0 +1,235 @@ +import { CSSProperties, forwardRef, useMemo } from 'react' +import { cx } from 'class-variance-authority' +import { useDroppable, useDraggable } from '@dnd-kit/core' +import { + DraggableAttributes, + DraggableSyntheticListeners, +} from '@dnd-kit/core/dist/hooks' + +type Data = { + id: string + isDraggable?: boolean + isDroppable?: boolean + onClick?: () => void +} + +export type Row = { + data: Data + context?: Context +} + +export type TableColumn = { + id: Columns + label: string + icon?: React.ReactNode + tip?: string + size?: number | string + cellClassName?: string + contentClassName?: string + render: React.FC> +} + +type Props = { + data: Data + context?: Context + columns: TableColumn[] + rowSize?: 'dense' | 'default' + focusId?: string + focusColor?: 'green' | 'red' | 'amber' | 'blue' | 'default' + getCellClassNames: ( + i: number, + className: string | undefined, + rounded?: boolean + ) => string + getContentClassNames: (i: number, className?: string) => string +} + +export function createTableRow< + Columns extends string, + D extends Data, + Context +>() { + const TableRow = forwardRef< + HTMLTableRowElement, + Props & { + className?: string + style?: CSSProperties + attributes?: DraggableAttributes + listeners?: DraggableSyntheticListeners + } + >( + ( + { + data, + style, + attributes, + listeners, + context, + columns, + rowSize = 'default', + focusId, + focusColor = 'default', + getCellClassNames, + getContentClassNames, + className, + }, + ref + ) => { + return ( + + {columns.map( + ( + { + id, + render: Render, + contentClassName: className, + cellClassName, + }, + i + ) => ( + +
+ +
+ + ) + )} + + ) + } + ) + return TableRow +} + +export function TableRowDraggable< + Columns extends string, + D extends Data, + Context +>({ + data, + context, + columns, + rowSize = 'default', + focusId, + focusColor = 'default', + getCellClassNames, + getContentClassNames, +}: Props) { + const { isDragging, attributes, listeners, setNodeRef, transform } = + useDraggable({ + id: data.id, + }) + const style = transform + ? { + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + } + : undefined + const TableRow = useMemo(() => createTableRow(), []) + + const className = isDragging + ? // ? // ? 'bg-gray-50 dark:bg-graydark-50 rounded ring ring-blue-200 dark:ring-blue-300' + // 'ring ring-blue-200 dark:ring-blue-300' + '' + : '' + + return ( + + ) +} + +export function TableRowDroppable< + Columns extends string, + D extends Data, + Context +>({ + data, + context, + columns, + rowSize = 'default', + focusId, + focusColor = 'default', + getCellClassNames, + getContentClassNames, +}: Props) { + const { isOver, setNodeRef } = useDroppable({ + id: data.id, + }) + const TableRow = useMemo(() => createTableRow(), []) + const className = isOver ? 'bg-blue-200/20 dark:bg-blue-300/20' : '' + + return ( + + ) +} diff --git a/libs/design-system/src/components/Table/index.tsx b/libs/design-system/src/components/Table/index.tsx new file mode 100644 index 000000000..d873ad301 --- /dev/null +++ b/libs/design-system/src/components/Table/index.tsx @@ -0,0 +1,333 @@ +import { Tooltip } from '../../core/Tooltip' +import { Panel } from '../../core/Panel' +import { Text } from '../../core/Text' +import { useCallback, useMemo } from 'react' +import { cx } from 'class-variance-authority' +import { CaretDown16, CaretUp16 } from '@siafoundation/react-icons' +import { times } from '@technically/lodash' +import { + DndContext, + DragStartEvent, + DragCancelEvent, + DragEndEvent, + DragOverEvent, + DragMoveEvent, + DragOverlay, + MouseSensor, + TouchSensor, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { + TableRowDraggable, + TableRowDroppable, + createTableRow, +} from './TableRow' + +type Data = { + id: string + isDraggable?: boolean + isDroppable?: boolean + onClick?: () => void +} + +export type Row = { + data: Data + context?: Context +} + +export type TableColumn = { + id: Columns + label: string + icon?: React.ReactNode + tip?: string + size?: number | string + cellClassName?: string + contentClassName?: string + render: React.FC> +} + +type Props< + Columns extends string, + SortField extends string, + D extends Data, + Context +> = { + data?: D[] + context?: Context + columns: TableColumn[] + sortField?: SortField + sortDirection?: 'asc' | 'desc' + toggleSort?: (field: SortField) => void + sortableColumns?: SortField[] + rowSize?: 'dense' | 'default' + pageSize: number + isLoading: boolean + emptyState?: React.ReactNode + focusId?: string + focusColor?: 'green' | 'red' | 'amber' | 'blue' | 'default' + onDragStart?: (e: DragStartEvent) => void + onDragOver?: (e: DragOverEvent) => void + onDragMove?: (e: DragMoveEvent) => void + onDragEnd?: (e: DragEndEvent) => void + onDragCancel?: (e: DragCancelEvent) => void + draggingDatum?: D +} + +export function Table< + Columns extends string, + SortField extends string, + D extends Data, + Context +>({ + columns, + data, + context, + sortField, + sortDirection, + sortableColumns, + toggleSort, + rowSize = 'default', + pageSize, + isLoading, + emptyState, + focusId, + focusColor = 'default', + onDragStart, + onDragOver, + onDragMove, + onDragEnd, + onDragCancel, + draggingDatum, +}: Props) { + let show = 'emptyState' + + if (isLoading && !data?.length) { + show = 'skeleton' + } + + if (data?.length) { + show = 'currentData' + } + + const getCellClassNames = useCallback( + (i: number, className: string | undefined, rounded?: boolean) => + cx( + i === 0 ? 'pl-6' : 'pl-4', + i === columns.length - 1 ? 'pr-6' : 'pr-4', + rounded + ? [ + i === 0 ? 'rounded-tl-lg' : '', + i === columns.length - 1 ? 'rounded-tr-lg' : '', + ] + : '', + className + ), + [columns] + ) + + const getContentClassNames = useCallback( + (i: number, className?: string) => cx('flex items-center', className), + [] + ) + + const TableRow = useMemo(() => createTableRow(), []) + + const mouseSensor = useSensor(MouseSensor, { + // Require the mouse to move by 10 pixels before activating + activationConstraint: { + distance: 10, + }, + }) + const touchSensor = useSensor(TouchSensor, { + // Press delay of 250ms, with tolerance of 5px of movement + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }) + + const sensors = useSensors(mouseSensor, touchSensor) + + return ( + + + {draggingDatum && ( + + + +
+
+ )} +
+ + + + + {columns.map( + ( + { id, icon, label, tip, cellClassName, contentClassName }, + i + ) => { + const isSortable = + sortableColumns?.includes(id as unknown as SortField) && + !!toggleSort + const isSortActive = (sortField as string) === id + return ( + + ) + } + )} + + + + {show === 'currentData' && + data?.map((row) => { + if (draggingDatum?.id === row.id) { + return null + } + + if (row.isDraggable) { + return ( + + ) + } + + if (row.isDroppable) { + return ( + + ) + } + return ( + + ) + })} + {show === 'skeleton' && + times(pageSize).map((i) => ( + + {columns.map(({ id, contentClassName, cellClassName }, i) => ( + + ))} + + ))} + +
+
+
{ + if (isSortable) { + toggleSort(id as unknown as SortField) + } + }} + className={cx( + getContentClassNames(i, contentClassName), + isSortable ? 'cursor-pointer' : '' + )} + > + + + {icon ?
{icon}
: null} + + {label} + +
+
+ {isSortActive && ( + + {sortDirection === 'asc' ? ( + + ) : ( + + )} + + )} + {isSortable && !isSortActive && ( + + + + )} + {/* {tip && {tip}} */} +
+
+
+
+
+ {show === 'emptyState' && emptyState} +
+
+ ) +} diff --git a/libs/react-renterd/src/bus.ts b/libs/react-renterd/src/bus.ts index 3a05349d7..23bfe55d3 100644 --- a/libs/react-renterd/src/bus.ts +++ b/libs/react-renterd/src/bus.ts @@ -600,11 +600,7 @@ export type RenameObjectRequest = { } export function useObjectRename( - args: HookArgsCallback< - { key: string; bucket: string }, - RenameObjectRequest, - never - > + args?: HookArgsCallback ) { return usePostFunc({ ...args, route: '/bus/objects/rename' }) } diff --git a/package-lock.json b/package-lock.json index 5040c45d9..bb1121cdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@carbon/icons-react": "^10.47.0", "@changesets/cli": "^2.25.0", + "@dnd-kit/core": "^6.1.0", "@ledgerhq/hw-transport-web-ble": "^6.27.19", "@ledgerhq/hw-transport-webhid": "^6.27.19", "@mdx-js/loader": "^2.1.1", @@ -3853,6 +3854,42 @@ "ms": "^2.1.1" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", + "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", + "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -30693,6 +30730,32 @@ } } }, + "@dnd-kit/accessibility": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", + "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@dnd-kit/core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", + "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "requires": { + "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + } + }, + "@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "requires": { + "tslib": "^2.0.0" + } + }, "@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", diff --git a/package.json b/package.json index 269976c59..2bf8a54b8 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@carbon/icons-react": "^10.47.0", "@changesets/cli": "^2.25.0", + "@dnd-kit/core": "^6.1.0", "@ledgerhq/hw-transport-web-ble": "^6.27.19", "@ledgerhq/hw-transport-webhid": "^6.27.19", "@mdx-js/loader": "^2.1.1",