[ENG-1483] Right Click -> New File (#2276)

* feat: Right Click -> New File

Creates an empty file called `Untitled` and adds `(1) -> (n)` for n number of files with the same `Untitled` name.

* feat: Seperate into Text File and Empty File

* chore: `cargo fmt`

* fix: Forgot to type convert to String

* feat: Working creation

* fix: i18n changes

* More i18n

* All added tags now translated

* With proper types checking now!

* Fix types

* chore: formatting
This commit is contained in:
Arnab Chakraborty
2024-04-04 00:20:31 -04:00
committed by GitHub
parent 232cbb33c2
commit 888ab5639b
17 changed files with 373 additions and 36 deletions

View File

@@ -1,5 +1,5 @@
use crate::{
api::utils::library,
api::{files::create_file, utils::library},
invalidate_query,
library::Library,
object::{
@@ -33,6 +33,14 @@ use super::{
};
const UNTITLED_FOLDER_STR: &str = "Untitled Folder";
const UNTITLED_FILE_STR: &str = "Untitled";
const UNTITLED_TEXT_FILE_STR: &str = "Untitled.txt";
#[derive(Type, Deserialize)]
enum EphemeralFileCreateContextTypes {
empty,
text,
}
pub(crate) fn mount() -> AlphaRouter<Ctx> {
R.router()
@@ -80,6 +88,33 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
},
)
})
.procedure("createFile", {
#[derive(Type, Deserialize)]
pub struct CreateEphemeralFileArgs {
pub path: PathBuf,
pub context: EphemeralFileCreateContextTypes,
pub name: Option<String>,
}
R.with2(library()).mutation(
|(_, library),
CreateEphemeralFileArgs {
mut path,
name,
context,
}: CreateEphemeralFileArgs| async move {
match context {
EphemeralFileCreateContextTypes::empty => {
path.push(name.as_deref().unwrap_or(UNTITLED_FILE_STR));
}
EphemeralFileCreateContextTypes::text => {
path.push(name.as_deref().unwrap_or(UNTITLED_TEXT_FILE_STR));
}
}
create_file(path, &library).await
},
)
})
.procedure("deleteFiles", {
R.with2(library())
.mutation(|(_, library), paths: Vec<PathBuf>| async move {

View File

@@ -46,6 +46,14 @@ use tracing::{error, warn};
use super::{Ctx, R};
const UNTITLED_FOLDER_STR: &str = "Untitled Folder";
const UNTITLED_FILE_STR: &str = "Untitled";
const UNTITLED_TEXT_FILE_STR: &str = "Untitled.txt";
#[derive(Type, Deserialize)]
enum FileCreateContextTypes {
empty,
text,
}
pub(crate) fn mount() -> AlphaRouter<Ctx> {
R.router()
@@ -294,6 +302,45 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
},
)
})
.procedure("createFile", {
#[derive(Type, Deserialize)]
pub struct CreateFileArgs {
pub location_id: location::id::Type,
pub sub_path: Option<PathBuf>,
pub name: Option<String>,
pub context: FileCreateContextTypes,
}
R.with2(library()).mutation(
|(_, library),
CreateFileArgs {
location_id,
sub_path,
context,
name,
}: CreateFileArgs| async move {
let mut path =
get_location_path_from_location_id(&library.db, location_id).await?;
if let Some(sub_path) = sub_path
.as_ref()
.and_then(|sub_path| sub_path.strip_prefix("/").ok())
{
path.push(sub_path);
}
match context {
FileCreateContextTypes::empty => {
path.push(name.as_deref().unwrap_or(UNTITLED_FILE_STR))
}
FileCreateContextTypes::text => {
path.push(name.as_deref().unwrap_or(UNTITLED_TEXT_FILE_STR))
}
}
create_file(path, &library).await
},
)
})
.procedure("updateAccessTime", {
R.with2(library())
.mutation(|(_, library), ids: Vec<i32>| async move {
@@ -872,6 +919,45 @@ pub(super) async fn create_directory(
.to_string())
}
pub(super) async fn create_file(
mut target_path: PathBuf,
library: &Library,
) -> Result<String, rspc::Error> {
match fs::metadata(&target_path).await {
Ok(metadata) if metadata.is_file() => {
target_path = find_available_filename_for_duplicate(&target_path).await?;
}
Ok(_) => {
return Err(FileSystemJobsError::WouldOverwrite(target_path.into_boxed_path()).into())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
// Everything is awesome!
}
Err(e) => {
return Err(FileIOError::from((
target_path,
e,
"Failed to access file system and get metadata on file to be created",
))
.into())
}
};
fs::File::create(&target_path)
.await
.map_err(|e| FileIOError::from((&target_path, e, "Failed to create file")))?;
invalidate_query!(library, "search.objects");
invalidate_query!(library, "search.paths");
invalidate_query!(library, "search.ephemeralPaths");
Ok(target_path
.file_name()
.expect("Failed to get file name")
.to_string_lossy()
.to_string())
}
#[derive(Type, Deserialize)]
pub struct FromPattern {
pub pattern: String,

View File

@@ -39,3 +39,10 @@ macro_rules! msgpack {
value
}}
}
// Only used for testing purposes. Do not use in production code.
use std::any::type_name;
pub fn test_type_of<T>(_: T) -> &'static str {
type_name::<T>()
}

View File

@@ -1,9 +1,11 @@
import {
Clipboard,
FilePlus,
FileX,
FolderPlus,
Hash,
Image,
Notepad,
Repeat,
Share,
ShieldCheck
@@ -40,21 +42,57 @@ export default (props: PropsWithChildren) => {
const cutEphemeralFiles = useLibraryMutation('ephemeralFiles.cutFiles');
const createFolder = useLibraryMutation(['files.createFolder'], {
onError: (e) => {
toast.error({ title: 'Error creating folder', body: `Error: ${e}.` });
toast.error({ title: t('create_folder_error'), body: `Error: ${e}.` });
console.error(e);
},
onSuccess: (folder) => {
toast.success({ title: `Created new folder "${folder}"` });
toast.success({
title: t("create_folder_success", {
name: folder
})
});
rescan();
}
});
const createFile = useLibraryMutation(['files.createFile'], {
onError: (e) => {
toast.error({ title: t('create_file_error'), body: `${e}.` });
console.error(e);
},
onSuccess: (file) => {
toast.success({
title: t("create_file_success", {
name: file
})
});
rescan();
}
});
const createEphemeralFolder = useLibraryMutation(['ephemeralFiles.createFolder'], {
onError: (e) => {
toast.error({ title: 'Error creating folder', body: `Error: ${e}.` });
toast.error({ title: t('create_folder_error'), body: `Error: ${e}.` });
console.error(e);
},
onSuccess: (folder) => {
toast.success({ title: `Created new folder "${folder}"` });
toast.success({
title: t("create_folder_success", {
name: folder
})
});
rescan();
}
});
const createEphemeralFile = useLibraryMutation(['ephemeralFiles.createFile'], {
onError: (e) => {
toast.error({ title: t('create_file_error'), body: `${e}.` });
console.error(e);
},
onSuccess: (file) => {
toast.success({
title: t("create_file_success", {
name: file
})
});
rescan();
}
});
@@ -89,25 +127,67 @@ export default (props: PropsWithChildren) => {
<CM.Separator />
</>
)}
<CM.Item
label={t('new_folder')}
icon={FolderPlus}
onClick={() => {
if (parent?.type === 'Location') {
createFolder.mutate({
location_id: parent.location.id,
sub_path: currentPath || null,
name: null
});
} else if (parent?.type === 'Ephemeral') {
createEphemeralFolder.mutate({
path: parent?.path,
name: null
});
}
}}
/>
<CM.SubMenu label={t('new')}>
<CM.Item
label={t('new_folder')}
icon={FolderPlus}
onClick={() => {
if (parent?.type === 'Location') {
createFolder.mutate({
location_id: parent.location.id,
sub_path: currentPath || null,
name: null
});
} else if (parent?.type === 'Ephemeral') {
createEphemeralFolder.mutate({
path: parent?.path,
name: null
});
}
}}
/>
<CM.Separator />
<CM.Item
label={t('text_file')}
icon={Notepad}
onClick={() => {
if (parent?.type === 'Location') {
createFile.mutate({
location_id: parent.location.id,
sub_path: currentPath || null,
name: null,
context: 'text'
});
} else if (parent?.type === 'Ephemeral') {
createEphemeralFile.mutate({
path: parent?.path,
context: 'text',
name: null
});
}
}}
/>
<CM.Item
label={t('empty_file')}
icon={FilePlus}
onClick={() => {
if (parent?.type === 'Location') {
createFile.mutate({
location_id: parent.location.id,
sub_path: currentPath || null,
name: null,
context: 'empty'
});
} else if (parent?.type === 'Ephemeral') {
createEphemeralFile.mutate({
path: parent?.path,
context: 'empty',
name: null
});
}
}}
/>
</CM.SubMenu>
</>
)}

View File

@@ -457,5 +457,12 @@
"search_for_files_and_actions": "Pesquisar por arquivos e ações...",
"toggle_command_palette": "Alternar paleta de comandos",
"pin": "шпілька",
"rescan": "паўторнае сканаванне"
"rescan": "паўторнае сканаванне",
"create_file_error": "Error creating file",
"create_file_success": "Created new file: {{name}}",
"create_folder_error": "Error creating folder",
"create_folder_success": "Created new folder: {{name}}",
"empty_file": "Empty file",
"new": "New",
"text_file": "Text File"
}

View File

@@ -452,5 +452,17 @@
"search_for_files_and_actions": "Nach Dateien und Aktionen suchen...",
"toggle_command_palette": "Befehlspalette umschalten",
"pin": "Stift",
"rescan": "Erneut scannen"
"rescan": "Erneut scannen",
"create_file_error": "Fehler beim Erstellen der Datei",
"create_file_success": "Neue Datei erstellt: {{name}}",
"create_folder_error": "Fehler beim Erstellen des Ordners",
"create_folder_success": "Neuer Ordner erstellt: {{name}}",
"empty_file": "Leere Akte",
"new": "Neu",
"size_b": "B",
"size_gb": "GB",
"size_kb": "kB",
"size_mb": "MB",
"size_tb": "TB",
"text_file": "Textdatei"
}

View File

@@ -294,7 +294,14 @@
"networking": "Networking",
"networking_port": "Networking Port",
"networking_port_description": "The port for Spacedrive's Peer-to-peer networking to communicate on. You should leave this disabled unless you have a restrictive firewall. Do not expose to the internet!",
"new_folder": "New folder",
"new": "New",
"new_folder": "Folder",
"text_file": "Text File",
"empty_file": "Empty file",
"create_folder_error":"Error creating folder",
"create_file_error":"Error creating file",
"create_folder_success":"Created new folder: {{name}}",
"create_file_success":"Created new file: {{name}}",
"new_library": "New library",
"new_location": "New location",
"new_location_web_description": "As you are using the browser version of Spacedrive you will (for now) need to specify an absolute URL of a directory local to the remote node.",

View File

@@ -452,5 +452,17 @@
"search_for_files_and_actions": "Buscar archivos y acciones...",
"toggle_command_palette": "Alternar paleta de comandos",
"pin": "Alfiler",
"rescan": "Volver a escanear"
"rescan": "Volver a escanear",
"create_file_error": "Error al crear el archivo",
"create_file_success": "Nuevo archivo creado: {{name}}",
"create_folder_error": "Error al crear la carpeta",
"create_folder_success": "Nueva carpeta creada: {{name}}",
"empty_file": "Archivo vacío",
"new": "Nuevo",
"size_b": "B",
"size_gb": "ES",
"size_kb": "kB",
"size_mb": "MEGABYTE",
"size_tb": "tuberculosis",
"text_file": "Archivo de texto"
}

View File

@@ -457,5 +457,12 @@
"search_for_files_and_actions": "Rechercher des fichiers et des actions...",
"toggle_command_palette": "Basculer la palette de commandes",
"pin": "Épingle",
"rescan": "Nouvelle analyse"
"rescan": "Nouvelle analyse",
"create_file_error": "Erreur lors de la création du fichier",
"create_file_success": "Nouveau fichier créé : {{name}}",
"create_folder_error": "Erreur lors de la création du dossier",
"create_folder_success": "Nouveau dossier créé : {{name}}",
"empty_file": "Fichier vide",
"new": "Nouveau",
"text_file": "Fichier texte"
}

View File

@@ -452,5 +452,17 @@
"version": "Versione {{versione}}",
"view_changes": "Visualizza modifiche",
"pin": "Spillo",
"rescan": "Nuova scansione"
"rescan": "Nuova scansione",
"create_file_error": "Errore durante la creazione del file",
"create_file_success": "Nuovo file creato: {{name}}",
"create_folder_error": "Errore durante la creazione della cartella",
"create_folder_success": "Creata una nuova cartella: {{name}}",
"empty_file": "File vuoto",
"new": "Nuovo",
"size_b": "B",
"size_gb": "GB",
"size_kb": "kB",
"size_mb": "MB",
"size_tb": "TBC",
"text_file": "File di testo"
}

View File

@@ -457,5 +457,12 @@
"search_for_files_and_actions": "ファイルとアクションを検索します...",
"toggle_command_palette": "コマンドパレットの切り替え",
"pin": "ピン",
"rescan": "再スキャン"
"rescan": "再スキャン",
"create_file_error": "ファイル作成エラー",
"create_file_success": "新しいファイルを作成しました: {{name}}",
"create_folder_error": "フォルダー作成エラー",
"create_folder_success": "新しいフォルダーを作成しました: {{name}}",
"empty_file": "空のファイル",
"new": "新しい",
"text_file": "テキストファイル"
}

View File

@@ -452,5 +452,17 @@
"search_for_files_and_actions": "Zoeken naar bestanden en acties...",
"toggle_command_palette": "Schakel het opdrachtpalet in of uit",
"pin": "Pin",
"rescan": "Opnieuw scannen"
"rescan": "Opnieuw scannen",
"create_file_error": "Fout bij aanmaken van bestand",
"create_file_success": "Nieuw bestand gemaakt: {{name}}",
"create_folder_error": "Fout bij maken van map",
"create_folder_success": "Nieuwe map gemaakt: {{name}}",
"empty_file": "Leeg bestand",
"new": "Nieuw",
"size_b": "B",
"size_gb": "GB",
"size_kb": "KB",
"size_mb": "MB",
"size_tb": "TB",
"text_file": "Tekstbestand"
}

View File

@@ -457,5 +457,12 @@
"search_for_files_and_actions": "Поиск файлов и действий...",
"toggle_command_palette": "Переключить палитру команд",
"pin": "приколоть",
"rescan": "Повторное сканирование"
"rescan": "Повторное сканирование",
"create_file_error": "Ошибка создания файла",
"create_file_success": "Создан новый файл: {{name}}",
"create_folder_error": "Ошибка создания папки",
"create_folder_success": "Создана новая папка: {{name}}.",
"empty_file": "Пустой файл",
"new": "Новый",
"text_file": "Текстовый файл"
}

View File

@@ -452,5 +452,17 @@
"search_for_files_and_actions": "Dosyaları ve eylemleri arayın...",
"toggle_command_palette": "Komut paletini değiştir",
"pin": "Toplu iğne",
"rescan": "Yeniden tara"
"rescan": "Yeniden tara",
"create_file_error": "Dosya oluşturulurken hata oluştu",
"create_file_success": "Yeni dosya oluşturuldu: {{name}}",
"create_folder_error": "Klasör oluşturulurken hata oluştu",
"create_folder_success": "Yeni klasör oluşturuldu: {{name}}",
"empty_file": "Boş dosya",
"new": "Yeni",
"size_b": "B",
"size_gb": "Büyük Britanya",
"size_kb": "kB",
"size_mb": "MB",
"size_tb": "verem",
"text_file": "Metin dosyası"
}

View File

@@ -452,5 +452,17 @@
"search_for_files_and_actions": "搜索文件和操作...",
"toggle_command_palette": "切换命令面板",
"pin": "别针",
"rescan": "重新扫描"
"rescan": "重新扫描",
"create_file_error": "创建文件时出错",
"create_file_success": "创建新文件:{{name}}",
"create_folder_error": "创建文件夹时出错",
"create_folder_success": "创建了新文件夹:{{name}}",
"empty_file": "空的文件",
"new": "新的",
"size_b": "乙",
"size_gb": "国标",
"size_kb": "千字节",
"size_mb": "MB",
"size_tb": "结核病",
"text_file": "文本文件"
}

View File

@@ -452,5 +452,17 @@
"search_for_files_and_actions": "搜尋文件和操作...",
"toggle_command_palette": "切換命令面板",
"pin": "別針",
"rescan": "重新掃描"
"rescan": "重新掃描",
"create_file_error": "建立文件時出錯",
"create_file_success": "建立新檔案:{{name}}",
"create_folder_error": "建立資料夾時出錯",
"create_folder_success": "建立了新資料夾:{{name}}",
"empty_file": "空的文件",
"new": "新的",
"size_b": "乙",
"size_gb": "國標",
"size_kb": "千位元組",
"size_mb": "MB",
"size_tb": "結核病",
"text_file": "文字檔案"
}

View File

@@ -69,12 +69,14 @@ export type Procedures = {
{ key: "cloud.locations.testing", input: TestingParams, result: null } |
{ key: "cloud.setApiOrigin", input: string, result: null } |
{ key: "ephemeralFiles.copyFiles", input: LibraryArgs<EphemeralFileSystemOps>, result: null } |
{ key: "ephemeralFiles.createFile", input: LibraryArgs<CreateEphemeralFileArgs>, result: string } |
{ key: "ephemeralFiles.createFolder", input: LibraryArgs<CreateEphemeralFolderArgs>, result: string } |
{ key: "ephemeralFiles.cutFiles", input: LibraryArgs<EphemeralFileSystemOps>, result: null } |
{ key: "ephemeralFiles.deleteFiles", input: LibraryArgs<string[]>, result: null } |
{ key: "ephemeralFiles.renameFile", input: LibraryArgs<EphemeralRenameFileArgs>, result: null } |
{ key: "files.convertImage", input: LibraryArgs<ConvertImageArgs>, result: null } |
{ key: "files.copyFiles", input: LibraryArgs<OldFileCopierJobInit>, result: null } |
{ key: "files.createFile", input: LibraryArgs<CreateFileArgs>, result: string } |
{ key: "files.createFolder", input: LibraryArgs<CreateFolderArgs>, result: string } |
{ key: "files.cutFiles", input: LibraryArgs<OldFileCutterJobInit>, result: null } |
{ key: "files.deleteFiles", input: LibraryArgs<OldFileDeleterJobInit>, result: null } |
@@ -201,8 +203,12 @@ export type ConvertImageArgs = { location_id: number; file_path_id: number; dele
export type ConvertibleExtension = "bmp" | "dib" | "ff" | "gif" | "ico" | "jpg" | "jpeg" | "png" | "pnm" | "qoi" | "tga" | "icb" | "vda" | "vst" | "tiff" | "tif" | "hif" | "heif" | "heifs" | "heic" | "heics" | "avif" | "avci" | "avcs" | "svg" | "svgz" | "pdf" | "webp"
export type CreateEphemeralFileArgs = { path: string; context: EphemeralFileCreateContextTypes; name: string | null }
export type CreateEphemeralFolderArgs = { path: string; name: string | null }
export type CreateFileArgs = { location_id: number; sub_path: string | null; name: string | null; context: FileCreateContextTypes }
export type CreateFolderArgs = { location_id: number; sub_path: string | null; name: string | null }
export type CreateLibraryArgs = { name: LibraryName; default_locations: DefaultLocations | null }
@@ -223,6 +229,8 @@ export type DoubleClickAction = "openFile" | "quickPreview"
export type EditLibraryArgs = { id: string; name: LibraryName | null; description: MaybeUndefined<string> }
export type EphemeralFileCreateContextTypes = "empty" | "text"
export type EphemeralFileSystemOps = { sources: string[]; target_dir: string }
export type EphemeralPathOrder = { field: "name"; value: SortOrder } | { field: "sizeInBytes"; value: SortOrder } | { field: "dateCreated"; value: SortOrder } | { field: "dateModified"; value: SortOrder }
@@ -254,6 +262,8 @@ export type ExplorerSettings<TOrder> = { layoutMode: ExplorerLayout | null; grid
export type Feedback = { message: string; emoji: number }
export type FileCreateContextTypes = "empty" | "text"
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; hidden: boolean | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null }
export type FilePathCursor = { isDir: boolean; variant: FilePathCursorVariant }