directory back up (parent) button + drag entry into parent to move

This commit is contained in:
2025-07-30 17:25:10 +02:00
parent 2c834eb42b
commit 3b141cc7cd
18 changed files with 243 additions and 95 deletions

View File

@@ -1,21 +1,29 @@
use thiserror::Error; use thiserror::Error;
use crate::domain::warren::models::file::AbsoluteFilePath; use crate::domain::warren::models::file::{AbsoluteFilePath, File};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LsRequest { pub struct LsRequest {
path: AbsoluteFilePath, path: AbsoluteFilePath,
include_parent: bool,
} }
impl LsRequest { impl LsRequest {
pub fn new(path: AbsoluteFilePath) -> Self { pub fn new(path: AbsoluteFilePath, include_parent: bool) -> Self {
Self { path } Self {
path,
include_parent,
}
} }
pub fn path(&self) -> &AbsoluteFilePath { pub fn path(&self) -> &AbsoluteFilePath {
&self.path &self.path
} }
pub fn include_parent(&self) -> bool {
self.include_parent
}
pub fn into_path(self) -> AbsoluteFilePath { pub fn into_path(self) -> AbsoluteFilePath {
self.path self.path
} }
@@ -28,3 +36,23 @@ pub enum LsError {
#[error(transparent)] #[error(transparent)]
Unknown(#[from] anyhow::Error), Unknown(#[from] anyhow::Error),
} }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LsResponse {
files: Vec<File>,
parent: Option<File>,
}
impl LsResponse {
pub fn new(files: Vec<File>, parent: Option<File>) -> Self {
Self { files, parent }
}
pub fn files(&self) -> &Vec<File> {
&self.files
}
pub fn parent(&self) -> Option<&File> {
self.parent.as_ref()
}
}

View File

@@ -4,11 +4,11 @@ use futures_util::StreamExt;
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use crate::domain::warren::models::file::LsResponse;
use crate::domain::warren::models::file::SaveResponse; use crate::domain::warren::models::file::SaveResponse;
use crate::domain::warren::models::file::{ use crate::domain::warren::models::file::{
AbsoluteFilePath, CatError, CatRequest, File, FileName, LsError, LsRequest, MkdirError, AbsoluteFilePath, CatError, CatRequest, FileName, LsError, LsRequest, MkdirError, MkdirRequest,
MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, TouchError, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, TouchError, TouchRequest,
TouchRequest,
}; };
use super::{Warren, WarrenName}; use super::{Warren, WarrenName};
@@ -77,12 +77,13 @@ impl WarrenLsRequest {
} }
pub fn build_fs_request(self, warren: &Warren) -> LsRequest { pub fn build_fs_request(self, warren: &Warren) -> LsRequest {
let include_parent = self.base.include_parent();
let path = warren let path = warren
.path() .path()
.clone() .clone()
.join(&self.base.into_path().to_relative()); .join(&self.base.into_path().to_relative());
LsRequest::new(path) LsRequest::new(path, include_parent)
} }
} }
@@ -96,16 +97,12 @@ impl Into<FetchWarrenRequest> for WarrenLsRequest {
pub struct WarrenLsResponse { pub struct WarrenLsResponse {
warren: Warren, warren: Warren,
path: AbsoluteFilePath, path: AbsoluteFilePath,
files: Vec<File>, base: LsResponse,
} }
impl WarrenLsResponse { impl WarrenLsResponse {
pub fn new(warren: Warren, path: AbsoluteFilePath, files: Vec<File>) -> Self { pub fn new(warren: Warren, path: AbsoluteFilePath, base: LsResponse) -> Self {
Self { Self { warren, path, base }
warren,
path,
files,
}
} }
pub fn warren(&self) -> &Warren { pub fn warren(&self) -> &Warren {
@@ -116,8 +113,8 @@ impl WarrenLsResponse {
&self.path &self.path
} }
pub fn files(&self) -> &Vec<File> { pub fn base(&self) -> &LsResponse {
&self.files &self.base
} }
} }

View File

@@ -15,7 +15,7 @@ use super::models::{
}, },
}, },
file::{ file::{
CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest, CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest,
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
TouchRequest, TouchRequest,
}, },
@@ -103,7 +103,7 @@ pub trait WarrenService: Clone + Send + Sync + 'static {
} }
pub trait FileSystemService: Clone + Send + Sync + 'static { pub trait FileSystemService: Clone + Send + Sync + 'static {
fn ls(&self, request: LsRequest) -> impl Future<Output = Result<Vec<File>, LsError>> + Send; fn ls(&self, request: LsRequest) -> impl Future<Output = Result<LsResponse, LsError>> + Send;
fn cat(&self, request: CatRequest) fn cat(&self, request: CatRequest)
-> impl Future<Output = Result<FileStream, CatError>> + Send; -> impl Future<Output = Result<FileStream, CatError>> + Send;
fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send; fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send;

View File

@@ -2,7 +2,7 @@ use uuid::Uuid;
use crate::domain::warren::models::{ use crate::domain::warren::models::{
auth_session::requests::FetchAuthSessionResponse, auth_session::requests::FetchAuthSessionResponse,
file::{AbsoluteFilePath, File}, file::{AbsoluteFilePath, LsResponse},
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User}, user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
user_warren::UserWarren, user_warren::UserWarren,
warren::{ warren::{
@@ -46,7 +46,7 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
} }
pub trait FileSystemNotifier: Clone + Send + Sync + 'static { pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
fn ls(&self, files: &Vec<File>) -> impl Future<Output = ()> + Send; fn ls(&self, response: &LsResponse) -> impl Future<Output = ()> + Send;
fn cat(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send; fn cat(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
fn mkdir(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send; fn mkdir(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
fn rm(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send; fn rm(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;

View File

@@ -7,7 +7,7 @@ use crate::domain::warren::models::{
}, },
}, },
file::{ file::{
CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest, CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest,
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
TouchRequest, TouchRequest,
}, },
@@ -66,7 +66,7 @@ pub trait WarrenRepository: Clone + Send + Sync + 'static {
} }
pub trait FileSystemRepository: Clone + Send + Sync + 'static { pub trait FileSystemRepository: Clone + Send + Sync + 'static {
fn ls(&self, request: LsRequest) -> impl Future<Output = Result<Vec<File>, LsError>> + Send; fn ls(&self, request: LsRequest) -> impl Future<Output = Result<LsResponse, LsError>> + Send;
fn cat(&self, request: CatRequest) fn cat(&self, request: CatRequest)
-> impl Future<Output = Result<FileStream, CatError>> + Send; -> impl Future<Output = Result<FileStream, CatError>> + Send;
fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send; fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send;

View File

@@ -1,6 +1,6 @@
use crate::domain::warren::{ use crate::domain::warren::{
models::file::{ models::file::{
CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest, CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest,
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
TouchRequest, TouchRequest,
}, },
@@ -40,7 +40,7 @@ where
M: FileSystemMetrics, M: FileSystemMetrics,
N: FileSystemNotifier, N: FileSystemNotifier,
{ {
async fn ls(&self, request: LsRequest) -> Result<Vec<File>, LsError> { async fn ls(&self, request: LsRequest) -> Result<LsResponse, LsError> {
let result = self.repository.ls(request).await; let result = self.repository.ls(request).await;
if let Ok(files) = result.as_ref() { if let Ok(files) = result.as_ref() {

View File

@@ -11,7 +11,7 @@ use crate::{
AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType, AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType,
LsRequest, LsRequest,
}, },
warren::WarrenLsRequest, warren::{WarrenLsRequest, WarrenLsResponse},
}, },
ports::{AuthService, WarrenService}, ports::{AuthService, WarrenService},
}, },
@@ -41,7 +41,10 @@ impl WarrenLsHttpRequestBody {
fn try_into_domain(self) -> Result<WarrenLsRequest, ParseWarrenLsHttpRequestError> { fn try_into_domain(self) -> Result<WarrenLsRequest, ParseWarrenLsHttpRequestError> {
let path = FilePath::new(&self.path)?.try_into()?; let path = FilePath::new(&self.path)?.try_into()?;
Ok(WarrenLsRequest::new(self.warren_id, LsRequest::new(path))) Ok(WarrenLsRequest::new(
self.warren_id,
LsRequest::new(path, &self.path != "/"),
))
} }
} }
@@ -75,6 +78,7 @@ pub struct WarrenFileElement {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ListWarrenFilesResponseData { pub struct ListWarrenFilesResponseData {
files: Vec<WarrenFileElement>, files: Vec<WarrenFileElement>,
parent: Option<WarrenFileElement>,
} }
impl From<&File> for WarrenFileElement { impl From<&File> for WarrenFileElement {
@@ -88,10 +92,16 @@ impl From<&File> for WarrenFileElement {
} }
} }
impl From<&Vec<File>> for ListWarrenFilesResponseData { impl From<WarrenLsResponse> for ListWarrenFilesResponseData {
fn from(value: &Vec<File>) -> Self { fn from(value: WarrenLsResponse) -> Self {
Self { Self {
files: value.iter().map(WarrenFileElement::from).collect(), files: value
.base()
.files()
.iter()
.map(WarrenFileElement::from)
.collect(),
parent: value.base().parent().map(WarrenFileElement::from),
} }
} }
} }
@@ -107,6 +117,6 @@ pub async fn list_warren_files<WS: WarrenService, AS: AuthService>(
.auth_service .auth_service
.auth_warren_ls(domain_request, state.warren_service.as_ref()) .auth_warren_ls(domain_request, state.warren_service.as_ref())
.await .await
.map(|response| ApiSuccess::new(StatusCode::OK, response.files().into())) .map(|response| ApiSuccess::new(StatusCode::OK, response.into()))
.map_err(ApiError::from) .map_err(ApiError::from)
} }

View File

@@ -15,8 +15,8 @@ use crate::{
models::{ models::{
file::{ file::{
AbsoluteFilePath, CatError, CatRequest, File, FileMimeType, FileName, FilePath, AbsoluteFilePath, CatError, CatRequest, File, FileMimeType, FileName, FilePath,
FileStream, FileType, LsError, LsRequest, MkdirError, MkdirRequest, MvError, FileStream, FileType, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest,
MvRequest, RelativeFilePath, RmError, RmRequest, SaveError, SaveRequest, MvError, MvRequest, RelativeFilePath, RmError, RmRequest, SaveError, SaveRequest,
SaveResponse, TouchError, TouchRequest, SaveResponse, TouchError, TouchRequest,
}, },
warren::UploadFileStream, warren::UploadFileStream,
@@ -75,13 +75,38 @@ impl FileSystem {
self.base_directory.join(&path.as_relative()) self.base_directory.join(&path.as_relative())
} }
async fn get_all_files(&self, absolute_path: &AbsoluteFilePath) -> anyhow::Result<Vec<File>> { async fn get_all_files(
let directory_path = self.get_target_path(absolute_path); &self,
absolute_path: &AbsoluteFilePath,
let mut dir = fs::read_dir(&directory_path).await?; include_parent: bool,
) -> anyhow::Result<LsResponse> {
let dir_path = self.get_target_path(absolute_path).as_ref().to_path_buf();
let mut files = Vec::new(); let mut files = Vec::new();
let parent = if include_parent {
let dir_name = FileName::new(
&dir_path
.file_name()
.context("Failed to get directory name")?
.to_owned()
.into_string()
.ok()
.context("Failed to get directory name")?,
)?;
Some(File::new(
dir_name,
FileType::Directory,
None,
get_btime(&dir_path),
))
} else {
None
};
let mut dir = fs::read_dir(&dir_path).await?;
while let Ok(Some(entry)) = dir.next_entry().await { while let Ok(Some(entry)) = dir.next_entry().await {
let name = entry let name = entry
.file_name() .file_name()
@@ -100,18 +125,7 @@ impl FileSystem {
} }
}; };
// TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5 let created_at = get_btime(entry.path());
// https://github.com/rust-lang/rust/pull/142682
let created_at = unsafe {
statx(
std::os::fd::BorrowedFd::borrow_raw(-100),
entry.path(),
rustix::fs::AtFlags::empty(),
rustix::fs::StatxFlags::BTIME,
)
}
.ok()
.map(|statx| statx.stx_btime.tv_sec as u64);
let mime_type = match file_type { let mime_type = match file_type {
FileType::File => FileMimeType::from_name(&name), FileType::File => FileMimeType::from_name(&name),
@@ -126,7 +140,7 @@ impl FileSystem {
)); ));
} }
Ok(files) Ok(LsResponse::new(files, parent))
} }
/// Actually created a directory in the underlying file system /// Actually created a directory in the underlying file system
@@ -233,13 +247,16 @@ impl FileSystem {
} }
impl FileSystemRepository for FileSystem { impl FileSystemRepository for FileSystem {
async fn ls(&self, request: LsRequest) -> Result<Vec<File>, LsError> { async fn ls(&self, request: LsRequest) -> Result<LsResponse, LsError> {
let files = self.get_all_files(request.path()).await.map_err(|err| { let files = self
anyhow!(err).context(format!( .get_all_files(request.path(), request.include_parent())
"Failed to get the files at path: {}", .await
request.path() .map_err(|err| {
)) anyhow!(err).context(format!(
})?; "Failed to get the files at path: {}",
request.path()
))
})?;
Ok(files) Ok(files)
} }
@@ -297,3 +314,21 @@ impl FileSystemRepository for FileSystem {
Ok(self.save(&path, &mut stream).await.map(SaveResponse::new)?) Ok(self.save(&path, &mut stream).await.map(SaveResponse::new)?)
} }
} }
// TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5
// https://github.com/rust-lang/rust/pull/142682
fn get_btime<P>(path: P) -> Option<u64>
where
P: rustix::path::Arg,
{
unsafe {
statx(
std::os::fd::BorrowedFd::borrow_raw(-100),
path,
rustix::fs::AtFlags::empty(),
rustix::fs::StatxFlags::BTIME,
)
}
.ok()
.map(|statx| statx.stx_btime.tv_sec as u64)
}

View File

@@ -3,7 +3,7 @@ use uuid::Uuid;
use crate::domain::warren::{ use crate::domain::warren::{
models::{ models::{
auth_session::requests::FetchAuthSessionResponse, auth_session::requests::FetchAuthSessionResponse,
file::{AbsoluteFilePath, File}, file::{AbsoluteFilePath, LsResponse},
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User}, user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
user_warren::UserWarren, user_warren::UserWarren,
warren::{ warren::{
@@ -57,7 +57,7 @@ impl WarrenNotifier for NotifierDebugLogger {
async fn warren_ls(&self, response: &WarrenLsResponse) { async fn warren_ls(&self, response: &WarrenLsResponse) {
tracing::debug!( tracing::debug!(
"[Notifier] Listed {} file(s) in warren {}", "[Notifier] Listed {} file(s) in warren {}",
response.files().len(), response.base().files().len(),
response.warren().name() response.warren().name()
); );
} }
@@ -101,8 +101,8 @@ impl WarrenNotifier for NotifierDebugLogger {
} }
impl FileSystemNotifier for NotifierDebugLogger { impl FileSystemNotifier for NotifierDebugLogger {
async fn ls(&self, files: &Vec<File>) { async fn ls(&self, response: &LsResponse) {
tracing::debug!("[Notifier] Listed {} file(s)", files.len()); tracing::debug!("[Notifier] Listed {} file(s)", response.files().len());
} }
async fn cat(&self, path: &AbsoluteFilePath) { async fn cat(&self, path: &AbsoluteFilePath) {
@@ -279,7 +279,7 @@ impl AuthNotifier for NotifierDebugLogger {
async fn auth_warren_ls(&self, user: &User, response: &WarrenLsResponse) { async fn auth_warren_ls(&self, user: &User, response: &WarrenLsResponse) {
tracing::debug!( tracing::debug!(
"[Notifier] Listed {} file(s) in warren {} for authenticated user {}", "[Notifier] Listed {} file(s) in warren {} for authenticated user {}",
response.files().len(), response.base().files().len(),
response.warren().name(), response.warren().name(),
user.id() user.id()
); );

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { DirectoryEntry } from '~/shared/types';
const { entry } = defineProps<{ entry: DirectoryEntry }>();
const warrenStore = useWarrenStore();
const onDrop = onDirectoryEntryDrop(entry, true);
</script>
<template>
<button
class="bg-accent/30 border-border flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none"
@contextmenu.prevent
@click="() => warrenStore.backCurrentPath()"
@drop="onDrop"
>
<div class="flex flex-row items-center">
<Icon class="size-6" name="lucide:folder-up" />
</div>
<div
class="flex w-full flex-col items-start justify-stretch gap-0 overflow-hidden text-left leading-6"
>
<span class="w-full truncate"
>..
<span class="text-muted-foreground truncate text-sm"
>({{ entry.name }})</span
></span
>
<NuxtTime
v-if="entry.createdAt != null"
:datetime="entry.createdAt * 1000"
class="text-muted-foreground w-full truncate text-sm"
relative
></NuxtTime>
</div>
</button>
</template>

View File

@@ -89,28 +89,7 @@ function onDragStart(e: DragEvent) {
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move';
} }
async function onDrop(e: DragEvent) { const onDrop = onDirectoryEntryDrop(entry);
if (e.dataTransfer == null || warrenStore.current == null) {
return;
}
if (entry.fileType !== 'directory') {
return;
}
const fileName = e.dataTransfer.getData('application/warren');
if (entry.name === fileName) {
return;
}
await moveFile(
warrenStore.current.warrenId,
warrenStore.current.path,
fileName,
`${warrenStore.current.path}/${entry.name}`
);
}
</script> </script>
<template> <template>
@@ -118,7 +97,7 @@ async function onDrop(e: DragEvent) {
<ContextMenuTrigger> <ContextMenuTrigger>
<button <button
:disabled="warrenStore.loading || disabled" :disabled="warrenStore.loading || disabled"
class="bg-accent/30 border-border select-none, flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2" class="bg-accent/30 border-border flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none"
draggable="true" draggable="true"
@dragstart="onDragStart" @dragstart="onDragStart"
@drop="onDrop" @drop="onDrop"

View File

@@ -2,8 +2,9 @@
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import type { DirectoryEntry } from '#shared/types'; import type { DirectoryEntry } from '#shared/types';
const { entries, isOverDropZone } = defineProps<{ const { entries, parent, isOverDropZone } = defineProps<{
entries: DirectoryEntry[]; entries: DirectoryEntry[];
parent: DirectoryEntry | null;
isOverDropZone?: boolean; isOverDropZone?: boolean;
}>(); }>();
@@ -23,6 +24,7 @@ const sortedEntries = computed(() =>
<Icon class="size-16 animate-pulse" name="lucide:upload" /> <Icon class="size-16 animate-pulse" name="lucide:upload" />
</div> </div>
<div class="flex flex-row flex-wrap gap-2"> <div class="flex flex-row flex-wrap gap-2">
<DirectoryBackEntry v-if="parent != null" :entry="parent" />
<DirectoryEntry <DirectoryEntry
v-for="entry in sortedEntries" v-for="entry in sortedEntries"
:key="entry.name" :key="entry.name"

View File

@@ -5,5 +5,5 @@ export function useWarrenPath() {
return null; return null;
} }
return `${store.current.warrenId}/${store.current.path}`; return `${store.current.warrenId}${store.current.path}`;
} }

View File

@@ -28,9 +28,9 @@ export async function getWarrens(): Promise<Record<string, WarrenData>> {
export async function getWarrenDirectory( export async function getWarrenDirectory(
warrenId: string, warrenId: string,
path: string path: string
): Promise<DirectoryEntry[]> { ): Promise<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }> {
const { data, error } = await useFetch< const { data, error } = await useFetch<
ApiResponse<{ files: DirectoryEntry[] }> ApiResponse<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }>
>(getApiUrl(`warrens/files/ls`), { >(getApiUrl(`warrens/files/ls`), {
method: 'POST', method: 'POST',
headers: getApiHeaders(), headers: getApiHeaders(),
@@ -44,9 +44,9 @@ export async function getWarrenDirectory(
throw error.value?.name; throw error.value?.name;
} }
const { files } = data.value.data; const { files, parent } = data.value.data;
return files; return { files, parent };
} }
export async function createDirectory( export async function createDirectory(

View File

@@ -27,7 +27,7 @@ if (warrenStore.current == null) {
}); });
} }
const entries = useAsyncData( const dirData = useAsyncData(
'current-directory', 'current-directory',
async () => { async () => {
if (warrenStore.current == null) { if (warrenStore.current == null) {
@@ -37,7 +37,7 @@ const entries = useAsyncData(
loadingIndicator.start(); loadingIndicator.start();
warrenStore.loading = true; warrenStore.loading = true;
const entries = await getWarrenDirectory( const { files, parent } = await getWarrenDirectory(
warrenStore.current.warrenId, warrenStore.current.warrenId,
warrenStore.current.path warrenStore.current.path
); );
@@ -45,7 +45,7 @@ const entries = useAsyncData(
warrenStore.loading = false; warrenStore.loading = false;
loadingIndicator.finish(); loadingIndicator.finish();
return entries; return { files, parent };
}, },
{ watch: [warrenPath] } { watch: [warrenPath] }
).data; ).data;
@@ -80,12 +80,13 @@ function onDrop(files: File[] | null, e: DragEvent) {
<div ref="dropZoneRef" class="grow"> <div ref="dropZoneRef" class="grow">
<DirectoryListContextMenu class="w-full grow"> <DirectoryListContextMenu class="w-full grow">
<DirectoryList <DirectoryList
v-if="entries != null" v-if="dirData != null"
:is-over-drop-zone=" :is-over-drop-zone="
dropZone.isOverDropZone.value && dropZone.isOverDropZone.value &&
dropZone.files.value != null dropZone.files.value != null
" "
:entries="entries" :entries="dirData.files"
:parent="dirData.parent"
/> />
</DirectoryListContextMenu> </DirectoryListContextMenu>
<RenameEntryDialog /> <RenameEntryDialog />

View File

@@ -11,6 +11,7 @@ export type DirectoryEntry = {
mimeType: string | null; mimeType: string | null;
/// Timestamp in seconds /// Timestamp in seconds
createdAt: number | null; createdAt: number | null;
isParent: boolean;
}; };
export type UploadStatus = export type UploadStatus =

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import type { DirectoryEntry } from '#shared/types'; import type { DirectoryEntry } from '#shared/types';
import type { WarrenData } from '#shared/types/warrens'; import type { WarrenData } from '#shared/types/warrens';
import { getParentPath } from '~/utils/files';
export const useWarrenStore = defineStore('warrens', { export const useWarrenStore = defineStore('warrens', {
state: () => ({ state: () => ({
@@ -29,6 +30,13 @@ export const useWarrenStore = defineStore('warrens', {
this.current.path += path; this.current.path += path;
}, },
backCurrentPath() {
if (this.current == null || this.current.path === '/') {
return;
}
this.current.path = getParentPath(this.current.path);
},
setCurrentWarrenPath(path: string) { setCurrentWarrenPath(path: string) {
if (this.current == null) { if (this.current == null) {
return; return;

49
frontend/utils/files.ts Normal file
View File

@@ -0,0 +1,49 @@
import { moveFile } from '~/lib/api/warrens';
import type { DirectoryEntry } from '~/shared/types';
export function getParentPath(path: string): string {
const sliceEnd = Math.max(1, path.lastIndexOf('/'));
return path.slice(0, sliceEnd);
}
export function onDirectoryEntryDrop(
entry: DirectoryEntry,
isParent: boolean = false
): (e: DragEvent) => void {
return async (e: DragEvent) => {
const warrenStore = useWarrenStore();
if (e.dataTransfer == null || warrenStore.current == null) {
return;
}
if (entry.fileType !== 'directory') {
return;
}
const fileName = e.dataTransfer.getData('application/warren');
if (entry.name === fileName) {
return;
}
let targetPath: string;
if (isParent) {
targetPath = getParentPath(warrenStore.current.path);
} else {
targetPath = warrenStore.current.path;
if (!targetPath.endsWith('/')) {
targetPath += '/';
}
targetPath += entry.name;
}
await moveFile(
warrenStore.current.warrenId,
warrenStore.current.path,
fileName,
targetPath
);
};
}