Skip to content

Commit

Permalink
feat: renterd rename files
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Feb 13, 2024
1 parent e253b3e commit e3a5929
Show file tree
Hide file tree
Showing 15 changed files with 322 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .changeset/modern-bags-exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'renterd': minor
---

Files can now be moved by dragging into or out of directories.
Files can now be moved by dragging into or out of directories. Closes https://github.com/SiaFoundation/renterd/issues/418
5 changes: 5 additions & 0 deletions .changeset/wet-mayflies-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

Files and directories can now be renamed via the context menu. Closes https://github.com/SiaFoundation/renterd/issues/418
21 changes: 19 additions & 2 deletions apps/renterd/components/Files/DirectoryContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
DropdownMenuLeftSlot,
DropdownMenuLabel,
} from '@siafoundation/design-system'
import { Delete16, FolderIcon } from '@siafoundation/react-icons'
import { Delete16, Edit16, FolderIcon } from '@siafoundation/react-icons'
import { useDirectoryDelete } from './useDirectoryDelete'
import { useDialog } from '../../contexts/dialog'

type Props = {
path: string
Expand All @@ -15,6 +16,7 @@ type Props = {

export function DirectoryContextMenu({ path, size }: Props) {
const directoryConfirmDelete = useDirectoryDelete()
const { openDialog } = useDialog()

return (
<DropdownMenu
Expand All @@ -23,9 +25,24 @@ export function DirectoryContextMenu({ path, size }: Props) {
<FolderIcon size={16} />
</Button>
}
contentProps={{ align: 'start' }}
contentProps={{
align: 'start',
onClick: (e) => {
e.stopPropagation()
},
}}
>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onSelect={() => {
openDialog('fileRename', path)
}}
>
<DropdownMenuLeftSlot>
<Edit16 />
</DropdownMenuLeftSlot>
Rename directory
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
directoryConfirmDelete(path, size)
Expand Down
9 changes: 9 additions & 0 deletions apps/renterd/components/Files/FileContextMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import {
Document16,
Warning16,
Filter16,
Edit16,
} from '@siafoundation/react-icons'
import { useFiles } from '../../../contexts/files'
import { useFileDelete } from '../useFileDelete'
import { CopyMetadataMenuItem } from './CopyMetadataMenuItem'
import { getFilename } from '../../../contexts/files/paths'
import { useDialog } from '../../../contexts/dialog'

type Props = {
path: string
Expand All @@ -29,6 +31,7 @@ type Props = {
export function FileContextMenu({ path }: Props) {
const { downloadFiles, getFileUrl, navigateToFile } = useFiles()
const deleteFile = useFileDelete()
const { openDialog } = useDialog()

return (
<DropdownMenu
Expand All @@ -50,6 +53,12 @@ export function FileContextMenu({ path }: Props) {
</DropdownMenuLeftSlot>
Download file
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => openDialog('fileRename', path)}>
<DropdownMenuLeftSlot>
<Edit16 />
</DropdownMenuLeftSlot>
Rename file
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => deleteFile(path)}>
<DropdownMenuLeftSlot>
<Delete16 />
Expand Down
137 changes: 137 additions & 0 deletions apps/renterd/components/Files/FileRenameDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
Dialog,
triggerErrorToast,
triggerSuccessToast,
ConfigFields,
useOnInvalid,
FormSubmitButton,
FieldText,
} from '@siafoundation/design-system'
import { useCallback, useEffect, useMemo } from 'react'
import { useForm } from 'react-hook-form'
import { useDialog } from '../../contexts/dialog'
import { useObjectRename } from '@siafoundation/react-renterd'
import { getFilename, isDirectory } from '../../contexts/files/paths'
import { getRenameFileRenameParams } from '../../contexts/files/rename'
import { useFiles } from '../../contexts/files'

function getDefaultValues(currentName: string) {
return {
name: currentName,
}
}

type Values = ReturnType<typeof getDefaultValues>

function getFields({
currentName,
}: {
currentName: string
}): ConfigFields<Values, never> {
return {
name: {
type: 'text',
title: 'Name',
placeholder: currentName,
validation: {
required: 'required',
validate: {
noSlash: (val) => {
if (val.includes('/')) {
return 'Name cannot contain slashes'
}
return true
},
},
},
},
}
}

type Props = {
id: string
trigger?: React.ReactNode
open: boolean
onOpenChange: (val: boolean) => void
}

// Renames a file or directory
export function FileRenameDialog({
id: originalPath,
trigger,
open,
onOpenChange,
}: Props) {
const { closeDialog } = useDialog()
const { refresh } = useFiles()

let name = getFilename(originalPath || '')
name = name.endsWith('/') ? name.slice(0, -1) : name
const defaultValues = getDefaultValues(name)

const objectRename = useObjectRename()
const form = useForm({
mode: 'all',
defaultValues,
})
// Reset the form when the name changes
useEffect(() => {
form.reset(getDefaultValues(name))
}, [form, name])

const onSubmit = useCallback(
async (values: Values) => {
const { bucket, to, from, mode } = getRenameFileRenameParams(
originalPath,
values.name
)
const response = await objectRename.post({
payload: {
bucket,
to,
from,
mode,
force: false,
},
})
if (response.error) {
triggerErrorToast(response.error)
} else {
refresh()
form.reset()
closeDialog()
triggerSuccessToast(
isDirectory(originalPath) ? 'Directory renamed.' : 'File renamed.'
)
}
},
[form, originalPath, refresh, objectRename, closeDialog]
)

const fields = useMemo(() => getFields({ currentName: name }), [name])

const onInvalid = useOnInvalid(fields)

return (
<Dialog
title="Rename file"
trigger={trigger}
open={open}
onOpenChange={(val) => {
if (!val) {
form.reset(defaultValues)
}
onOpenChange(val)
}}
contentVariants={{
className: 'w-[400px]',
}}
onSubmit={form.handleSubmit(onSubmit, onInvalid)}
>
<div className="flex flex-col gap-4">
<FieldText name="name" form={form} fields={fields} autoComplete="off" />
<FormSubmitButton form={form}>Save</FormSubmitButton>
</div>
</Dialog>
)
}
8 changes: 8 additions & 0 deletions apps/renterd/contexts/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { HostsFilterPublicKeyDialog } from '../components/Hosts/HostsFilterPubli
import { FilesBucketDeleteDialog } from '../components/Files/FilesBucketDeleteDialog'
import { FilesBucketPolicyDialog } from '../components/Files/FilesBucketPolicyDialog'
import { FilesBucketCreateDialog } from '../components/Files/FilesBucketCreateDialog'
import { FileRenameDialog } from '../components/Files/FileRenameDialog'
import { KeysCreateDialog } from '../components/Keys/KeysCreateDialog'

export type DialogType =
Expand All @@ -44,6 +45,7 @@ export type DialogType =
| 'filesCreateDirectory'
| 'filesBucketPolicy'
| 'filesSearch'
| 'fileRename'
| 'keysCreate'
| 'alerts'
| 'confirm'
Expand Down Expand Up @@ -121,6 +123,7 @@ export function DialogProvider({ children }: Props) {

export function Dialogs() {
const {
id,
dialog,
openDialog,
onOpenChange,
Expand Down Expand Up @@ -180,6 +183,11 @@ export function Dialogs() {
open={dialog === 'filesSearch'}
onOpenChange={(val) => (val ? openDialog(dialog) : closeDialog())}
/>
<FileRenameDialog
id={id}
open={dialog === 'fileRename'}
onOpenChange={(val) => (val ? openDialog(dialog) : closeDialog())}
/>
<HostsAllowBlockDialog
open={dialog === 'hostsManageAllowBlock'}
onOpenChange={(val) => (val ? openDialog(dialog) : closeDialog())}
Expand Down
14 changes: 6 additions & 8 deletions apps/renterd/contexts/files/dataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import { useContracts } from '../contracts'
import { ObjectData, SortField } from './types'
import {
bucketAndKeyParamsFromPath,
bucketAndResponseKeyToFilePath,
getBucketFromPath,
getDirPath,
buildDirectoryPath,
getFilename,
getFilePath,
join,
isDirectory,
} from './paths'
import {
Expand Down Expand Up @@ -101,7 +100,7 @@ export function useDataset({
if (!activeBucket) {
buckets.data?.forEach((bucket) => {
const name = bucket.name
const path = getDirPath(name, '')
const path = buildDirectoryPath(name, '')
dataMap[name] = {
id: path,
path,
Expand All @@ -117,7 +116,7 @@ export function useDataset({
})
} else if (response.data) {
response.data.entries?.forEach(({ name: key, size, health }) => {
const path = bucketAndResponseKeyToFilePath(activeBucketName, key)
const path = join(activeBucketName, key)
const name = getFilename(key)
dataMap[path] = {
id: path,
Expand All @@ -135,9 +134,7 @@ export function useDataset({
}
})
uploadsList
.filter(
({ path, name }) => path === getFilePath(activeDirectoryPath, name)
)
.filter(({ path, name }) => path === join(activeDirectoryPath, name))
.forEach((upload) => {
dataMap[upload.path] = upload
})
Expand All @@ -161,5 +158,6 @@ export function useDataset({
offset,
response,
dataset: d.data,
refresh: response.mutate,
}
}
5 changes: 3 additions & 2 deletions apps/renterd/contexts/files/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function useFilesMain() {
const { downloadFiles, downloadsList, getFileUrl, downloadCancel } =
useDownloads()

const { limit, offset, response, dataset } = useDataset({
const { limit, offset, response, refresh, dataset } = useDataset({
activeDirectoryPath,
setActiveDirectory,
uploadsList,
Expand All @@ -101,7 +101,7 @@ function useFilesMain() {
dataset,
activeDirectory,
setActiveDirectory,
mutate: response.mutate,
refresh,
})

// Add parent directory to the dataset
Expand Down Expand Up @@ -196,6 +196,7 @@ function useFilesMain() {
activeDirectoryPath,
navigateToFile,
dataState,
refresh,
limit,
offset,
datasetPage,
Expand Down
Loading

0 comments on commit e3a5929

Please sign in to comment.