[ENG-1365] Move to Trash (#2318)

* Working System Trash Connection

* small changes and prettier

---------

Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
Arnab Chakraborty 2024-04-12 01:01:37 -04:00 committed by GitHub
parent 9f5396133b
commit 66063d22c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 233 additions and 52 deletions

View file

@ -172,6 +172,7 @@ Also ensure that Rosetta is installed, as a few of our dependencies require it.
#### `ModuleNotFoundError: No module named 'distutils'`
If you run into this issue, or some other error involving `node-gyp`:
```
File "pnpm@8.15.6/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py", line 42, in <module>
import gyp # noqa: E402

26
Cargo.lock generated
View file

@ -8276,6 +8276,7 @@ dependencies = [
"tracing-appender",
"tracing-subscriber",
"tracing-test",
"trash",
"uuid",
"webp",
]
@ -10531,6 +10532,22 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "trash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1a7a9a17d3b004898be42be29a4c18d5a4cf008b5cdf72d69b1945dfcb158a"
dependencies = [
"chrono",
"libc",
"log",
"objc",
"once_cell",
"scopeguard",
"url",
"windows 0.44.0",
]
[[package]]
name = "treediff"
version = "4.0.2"
@ -11267,6 +11284,15 @@ dependencies = [
"windows_x86_64_msvc 0.39.0",
]
[[package]]
name = "windows"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows"
version = "0.48.0"

View file

@ -132,6 +132,7 @@ static_assertions = "1.1.0"
sysinfo = "0.29.10"
tar = "0.4.40"
tower-service = "0.3.2"
trash = "4.1.0"
# Override features of transitive dependencies
[dependencies.openssl]

View file

@ -27,6 +27,7 @@ use specta::Type;
use tokio::{fs, io};
use tokio_stream::{wrappers::ReadDirStream, StreamExt};
use tracing::{error, warn};
use trash;
use super::{
files::{create_directory, FromPattern},
@ -147,6 +148,35 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
Ok(())
})
})
.procedure("moveToTrash", {
R.with2(library())
.mutation(|(_, library), paths: Vec<PathBuf>| async move {
paths
.into_iter()
.map(|path| async move {
match fs::metadata(&path).await {
Ok(_) => {
trash::delete(&path).unwrap();
Ok(())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(FileIOError::from((
path,
e,
"Failed to get file metadata for deletion",
))),
}
})
.collect::<Vec<_>>()
.try_join()
.await?;
invalidate_query!(library, "search.ephemeralPaths");
Ok(())
})
})
.procedure("copyFiles", {
R.with2(library())
.mutation(|(_, library), args: EphemeralFileSystemOps| async move {

View file

@ -44,6 +44,7 @@ use serde::{Deserialize, Serialize};
use specta::Type;
use tokio::{fs, io, task::spawn_blocking};
use tracing::{error, warn};
use trash;
use super::{Ctx, R};
@ -515,6 +516,53 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
}
})
})
.procedure("moveToTrash", {
R.with2(library())
.mutation(|(node, library), args: OldFileDeleterJobInit| async move {
match args.file_path_ids.len() {
0 => Ok(()),
1 => {
let (maybe_location, maybe_file_path) = library
.db
._batch((
library
.db
.location()
.find_unique(location::id::equals(args.location_id))
.select(location::select!({ path })),
library
.db
.file_path()
.find_unique(file_path::id::equals(args.file_path_ids[0]))
.select(file_path_to_isolate::select()),
))
.await?;
let location_path = maybe_location
.ok_or(LocationError::IdNotFound(args.location_id))?
.path
.ok_or(LocationError::MissingPath(args.location_id))?;
let file_path = maybe_file_path.ok_or(LocationError::FilePath(
FilePathError::IdNotFound(args.file_path_ids[0]),
))?;
let full_path = Path::new(&location_path).join(
IsolatedFilePathData::try_from(&file_path)
.map_err(LocationError::MissingField)?,
);
trash::delete(&full_path).unwrap();
Ok(())
}
_ => Job::new(args)
.spawn(&node, &library)
.await
.map_err(Into::into),
}
})
})
.procedure("convertImage", {
#[derive(Type, Deserialize)]
struct ConvertImageArgs {

View file

@ -1,4 +1,8 @@
import { AppWindow, ArrowSquareOut, CaretRight, ClipboardText } from '@phosphor-icons/react';
import clsx from 'clsx';
import { memo, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { createSearchParams } from 'react-router-dom';
import {
getExplorerItemData,
getIndexedItemFilePath,
@ -6,13 +10,9 @@ import {
useLibraryQuery
} from '@sd/client';
import { ContextMenu } from '@sd/ui';
import clsx from 'clsx';
import { memo, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { createSearchParams } from 'react-router-dom';
import { useTabsContext } from '~/TabsContext';
import { Icon } from '~/components';
import { useIsDark, useLocale, useOperatingSystem } from '~/hooks';
import { useTabsContext } from '~/TabsContext';
import { usePlatform } from '~/util/Platform';
import { useExplorerContext } from './Context';
@ -240,7 +240,7 @@ const Path = ({ path, onClick, disabled, locationPath }: PathProps) => {
<ContextMenu.Item
onClick={() => navigator.clipboard.writeText(osPath)}
icon={ClipboardText}
label={t("copy_as_path")}
label={t('copy_as_path')}
/>
</ContextMenu.Root>
);

View file

@ -47,6 +47,8 @@ export default (props: Props) => {
const { t } = useLocale();
const deleteFile = useLibraryMutation('files.deleteFiles');
const deleteEphemeralFile = useLibraryMutation('ephemeralFiles.deleteFiles');
const moveToTrashFile = useLibraryMutation('files.moveToTrash');
const moveToTrashEphemeralFile = useLibraryMutation('ephemeralFiles.moveToTrash');
const form = useZodForm();
const { dirCount = 0, fileCount = 0, indexedArgs, ephemeralArgs } = props;
@ -76,23 +78,46 @@ export default (props: Props) => {
await deleteEphemeralFile.mutateAsync(paths);
}
})}
onSubmitSecond={form.handleSubmit(async () => {
if (indexedArgs != undefined) {
console.log(
'DEBUG: DeleteDialog.tsx: onSubmitSecond (Move to Trash) -> Indexed Files'
);
const { locationId, rescan, pathIds } = indexedArgs;
await moveToTrashFile.mutateAsync({
location_id: locationId,
file_path_ids: pathIds
});
rescan?.();
}
if (ephemeralArgs != undefined) {
console.log(
'DEBUG: DeleteDialog.tsx: onSubmitSecond (Move to Trash) -> Ephemeral Files'
);
const { paths } = ephemeralArgs;
await moveToTrashEphemeralFile.mutateAsync(paths);
}
})}
icon={<Icon theme="light" name={icon} size={28} />}
dialog={useDialog(props)}
title={t('delete_dialog_title', { prefix, type })}
description={description}
loading={deleteFile.isLoading}
ctaLabel={t('delete')}
ctaLabel={t('delete_forever')}
ctaSecondLabel={t('move_to_trash')}
ctaDanger
className="w-[200px]"
>
<Tooltip label={t('coming_soon')}>
{/* <Tooltip label={t('coming_soon')}>
<div className="flex items-center pt-2 opacity-50">
<CheckBox disabled className="!mt-0" />
<p className="text-sm text-ink-dull">
Delete all matching {type.endsWith('s') ? type : type + 's'}
</p>
</div>
</Tooltip>
</Tooltip> */}
</Dialog>
);
};

View file

@ -104,7 +104,7 @@
"delete_rule": "Выдаліць правіла",
"delete_tag": "Выдаліць тэг",
"delete_tag_description": "Вы ўпэўнены, што хочаце выдаліць гэты тэг? Гэта дзеянне няможна скасаваць, і тэгнутые файлы будуць адлучаны.",
"delete_warning": "Папярэджанне: гэта выдаліць ваш {{type}} назаўжды, у нас пакуль няма сметніцы.",
"delete_warning": "гэта выдаліць ваш {{type}} назаўжды, у нас пакуль няма сметніцы.",
"description": "Апісанне",
"deselect": "Скасаваць выбар",
"details": "Падрабязней",

View file

@ -104,7 +104,7 @@
"delete_rule": "Regel löschen",
"delete_tag": "Tag löschen",
"delete_tag_description": "Sind Sie sicher, dass Sie diesen Tag löschen möchten? Dies kann nicht rückgängig gemacht werden, und getaggte Dateien werden nicht mehr verlinkt.",
"delete_warning": "Warnung: Dies wird Ihre {{type}} für immer löschen, wir haben noch keinen Papierkorb...",
"delete_warning": "Dies wird Ihre {{type}} für immer löschen, wir haben noch keinen Papierkorb...",
"description": "Beschreibung",
"deselect": "Abwählen",
"details": "Details",

View file

@ -64,6 +64,10 @@
"copy_path_to_clipboard": "Copy path to clipboard",
"copy_success": "Items copied",
"create": "Create",
"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}}",
"create_library": "Create a Library",
"create_library_description": "Libraries are a secure, on-device database. Your files remain where they are, the Library catalogs them and stores all Spacedrive related data.",
"create_new_library": "Create new library",
@ -82,11 +86,16 @@
"cut_object": "Cut object",
"cut_success": "Items cut",
"data_folder": "Data Folder",
"date_accessed": "Date Accessed",
"date_created": "Date Created",
"date_indexed": "Date Indexed",
"date_modified": "Date Modified",
"debug_mode": "Debug mode",
"debug_mode_description": "Enable extra debugging features within the app.",
"default": "Default",
"delete": "Delete",
"delete_dialog_title": "Delete {{prefix}} {{type}}",
"delete_forever": "Delete Forever",
"delete_info": "This will not delete the actual folder on disk. Preview media will be deleted.",
"delete_library": "Delete Library",
"delete_library_description": "This is permanent, your files will not be deleted, only the Spacedrive library.",
@ -96,7 +105,7 @@
"delete_rule": "Delete rule",
"delete_tag": "Delete Tag",
"delete_tag_description": "Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked.",
"delete_warning": "Warning: This will delete your {{type}} forever, we don't have a trash can yet...",
"delete_warning": "This will delete your {{type}}. This action cannot be undone as of right now. If you Move to Trash, you can restore it later. If you Delete Forever, it will be gone forever.",
"description": "Description",
"deselect": "Deselect",
"details": "Details",
@ -121,7 +130,10 @@
"edit": "Edit",
"edit_library": "Edit Library",
"edit_location": "Edit Location",
"empty_file": "Empty file",
"enable_networking": "Enable Networking",
"enable_networking_description": "Allow your node to communicate with other Spacedrive nodes around you.",
"enable_networking_description_required": "Required for library sync or Spacedrop!",
"encrypt": "Encrypt",
"encrypt_library": "Encrypt Library",
"encrypt_library_coming_soon": "Library encryption coming soon",
@ -200,6 +212,7 @@
"hide_in_sidebar_description": "Prevent this tag from showing in the sidebar of the app.",
"hide_location_from_view": "Hide location and contents from view",
"home": "Home",
"icon_size": "Icon size",
"image_labeler_ai_model": "Image label recognition AI model",
"image_labeler_ai_model_description": "The model used to recognize objects in images. Larger models are more accurate but slower.",
"import": "Import",
@ -211,8 +224,6 @@
"install_update": "Install Update",
"installed": "Installed",
"item_size": "Item size",
"icon_size": "Icon size",
"text_size": "Text size",
"item_with_count_one": "{{count}} item",
"item_with_count_other": "{{count}} items",
"job_has_been_canceled": "Job has been canceled.",
@ -250,6 +261,7 @@
"location_connected_tooltip": "Location is being watched for changes",
"location_disconnected_tooltip": "Location is not being watched for changes",
"location_display_name_info": "The name of this Location, this is what will be displayed in the sidebar. Will not rename the actual folder on disk.",
"location_empty_notice_message": "No files found here",
"location_is_already_linked": "Location is already linked",
"location_path_info": "The path to this Location, this is where the files will be stored on disk.",
"location_type": "Location Type",
@ -259,9 +271,11 @@
"locations": "Locations",
"locations_description": "Manage your storage locations.",
"lock": "Lock",
"log_in": "Log in",
"log_in_with_browser": "Log in with browser",
"log_out": "Log out",
"logged_in_as": "Logged in as {{email}}",
"logging_in": "Logging in...",
"logout": "Logout",
"manage_library": "Manage Library",
"managed": "Managed",
@ -279,6 +293,7 @@
"move_back_within_quick_preview": "Move back within quick preview",
"move_files": "Move Files",
"move_forward_within_quick_preview": "Move forward within quick preview",
"move_to_trash": "Move to Trash",
"name": "Name",
"navigate_back": "Navigate back",
"navigate_backwards": "Navigate backwards",
@ -296,19 +311,14 @@
"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": "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.",
"new_tab": "New Tab",
"new_tag": "New tag",
"new_update_available": "New Update Available!",
"location_empty_notice_message": "No files found here",
"no_favorite_items": "No favorite items",
"no_items_found": "No items found",
"no_jobs": "No jobs.",
"no_labels": "No labels",
"no_nodes_found": "No Spacedrive nodes were found.",
@ -326,6 +336,7 @@
"online": "Online",
"open": "Open",
"open_file": "Open File",
"open_in_new_tab": "Open in new tab",
"open_new_location_once_added": "Open new location once added",
"open_new_tab": "Open new tab",
"open_object": "Open object",
@ -347,12 +358,14 @@
"pause": "Pause",
"peers": "Peers",
"people": "People",
"pin": "Pin",
"privacy": "Privacy",
"privacy_description": "Spacedrive is built for privacy, that's why we're open source and local first. So we'll make it very clear what data is shared with us.",
"quick_preview": "Quick Preview",
"quick_view": "Quick view",
"recent_jobs": "Recent Jobs",
"recents": "Recents",
"recents_notice_message": "Recents are created when you open a file.",
"regen_labels": "Regen Labels",
"regen_thumbnails": "Regen Thumbnails",
"regenerate_thumbs": "Regenerate Thumbs",
@ -364,6 +377,7 @@
"rename": "Rename",
"rename_object": "Rename object",
"replica": "Replica",
"rescan": "Rescan",
"rescan_directory": "Rescan Directory",
"rescan_location": "Rescan Location",
"reset": "Reset",
@ -377,6 +391,7 @@
"save": "Save",
"save_changes": "Save Changes",
"saved_searches": "Saved Searches",
"search": "Search",
"search_extensions": "Search extensions",
"search_for_files_and_actions": "Search for files and actions...",
"secure_delete": "Secure delete",
@ -399,9 +414,9 @@
"show_slider": "Show slider",
"size": "Size",
"size_b": "B",
"size_gb": "GB",
"size_kb": "kB",
"size_mb": "MB",
"size_gb": "GB",
"size_tb": "TB",
"skip_login": "Skip login",
"sort_by": "Sort by",
@ -429,9 +444,12 @@
"sync_with_library_description": "If enabled, your keybinds will be synced with library, otherwise they will apply only to this client.",
"tags": "Tags",
"tags_description": "Manage your tags.",
"tags_notice_message": "No items assigned to this tag.",
"telemetry_description": "Toggle ON to provide developers with detailed usage and telemetry data to enhance the app. Toggle OFF to send only basic data: your activity status, app version, core version, and platform (e.g., mobile, web, or desktop).",
"telemetry_title": "Share Additional Telemetry and Usage Data",
"temperature": "Temperature",
"text_file": "Text File",
"text_size": "Text size",
"thank_you_for_your_feedback": "Thanks for your feedback!",
"thumbnailer_cpu_usage": "Thumbnailer CPU usage",
"thumbnailer_cpu_usage_description": "Limit how much CPU the thumbnailer can use for background processing.",
@ -462,21 +480,5 @@
"your_account": "Your account",
"your_account_description": "Spacedrive account and information.",
"your_local_network": "Your Local Network",
"your_privacy": "Your Privacy",
"pin": "Pin",
"rescan": "Rescan",
"open_in_new_tab": "Open in new tab",
"enable_networking_description": "Allow your node to communicate with other Spacedrive nodes around you.",
"enable_networking_description_required": "Required for library sync or Spacedrop!",
"log_in": "Log in",
"logging_in": "Logging in...",
"no_favorite_items": "No favorite items",
"tags_notice_message": "No items assigned to this tag.",
"recents_notice_message": "Recents are created when you open a file.",
"no_items_found": "No items found",
"search": "Search",
"date_created": "Date Created",
"date_modified": "Date Modified",
"date_indexed": "Date Indexed",
"date_accessed": "Date Accessed"
}
"your_privacy": "Your Privacy"
}

View file

@ -104,7 +104,7 @@
"delete_rule": "Eliminar regla",
"delete_tag": "Eliminar Etiqueta",
"delete_tag_description": "¿Estás seguro de que quieres eliminar esta etiqueta? Esto no se puede deshacer y los archivos etiquetados serán desvinculados.",
"delete_warning": "Advertencia: Esto eliminará tu {{type}} para siempre, aún no tenemos papelera...",
"delete_warning": "Esto eliminará tu {{type}} para siempre, aún no tenemos papelera...",
"description": "Descripción",
"deselect": "Deseleccionar",
"details": "Detalles",

View file

@ -104,7 +104,7 @@
"delete_rule": "Supprimer la règle",
"delete_tag": "Supprimer l'étiquette",
"delete_tag_description": "Êtes-vous sûr de vouloir supprimer cette étiquette ? Cela ne peut pas être annulé et les fichiers étiquetés seront dissociés.",
"delete_warning": "Attention : Ceci supprimera votre {{type}} pour toujours, nous n'avons pas encore de corbeille...",
"delete_warning": "Ceci supprimera votre {{type}} pour toujours, nous n'avons pas encore de corbeille...",
"description": "Description",
"deselect": "Désélectionner",
"details": "Détails",

View file

@ -104,7 +104,7 @@
"delete_rule": "Elimina regola",
"delete_tag": "Elimina Tag",
"delete_tag_description": "Sei sicuro di voler cancellare questa etichetta? Questa operazione non può essere annullata e i file etichettati verranno scollegati.",
"delete_warning": "Attenzione: stai per eliminare il tuo {{type}} per sempre, non abbiamo ancora un cestino...",
"delete_warning": "stai per eliminare il tuo {{type}} per sempre, non abbiamo ancora un cestino...",
"description": "Descrizione",
"deselect": "Deseleziona",
"details": "Dettagli",

View file

@ -104,7 +104,7 @@
"delete_rule": "ルールを削除",
"delete_tag": "タグを削除",
"delete_tag_description": "本当にこのタグを削除しますか?これを元に戻すことはできず、タグ付けされたファイル間の結びつきは失われます。",
"delete_warning": "【警告】これはあなたの {{type}} を完全に削除します。",
"delete_warning": "これはあなたの {{type}} を完全に削除します。",
"description": "説明",
"deselect": "クリップボードを空にする",
"details": "詳細",

View file

@ -104,7 +104,7 @@
"delete_rule": "Verwijder regel",
"delete_tag": "Verwijder Tag",
"delete_tag_description": "Weet je zeker dat je deze tag wilt verwijderen? Dit kan niet ongedaan worden gemaakt en ge-tagde bestanden worden ontkoppeld.",
"delete_warning": "Waarschuwing: hiermee wordt je {{type}} permanent verwijderd, we hebben nog geen prullenbak...",
"delete_warning": "hiermee wordt je {{type}} permanent verwijderd, we hebben nog geen prullenbak...",
"description": "Omschrijving",
"deselect": "Deselecteer",
"details": "Details",

View file

@ -104,7 +104,7 @@
"delete_rule": "Удалить правило",
"delete_tag": "Удалить тег",
"delete_tag_description": "Вы уверены, что хотите удалить этот тег? Это действие нельзя отменить, и тегнутые файлы будут отсоединены.",
"delete_warning": "Предупреждение: это удалит ваш {{type}} навсегда, у нас пока нет мусорной корзины.",
"delete_warning": "это удалит ваш {{type}} навсегда, у нас пока нет мусорной корзины.",
"description": "Описание",
"deselect": "Отменить выбор",
"details": "Подробности",

View file

@ -104,7 +104,7 @@
"delete_rule": "Kuralı Sil",
"delete_tag": "Etiketi Sil",
"delete_tag_description": "Bu etiketi silmek istediğinizden emin misiniz? Bu geri alınamaz ve etiketli dosyalar bağlantısız kalacak.",
"delete_warning": "Uyarı: Bu, {{type}}'ınızı sonsuza dek silecek, henüz çöp kutumuz yok...",
"delete_warning": "Bu, {{type}}'ınızı sonsuza dek silecek, henüz çöp kutumuz yok...",
"description": "Açıklama",
"deselect": "Seçimi Kaldır",
"details": "Detaylar",

View file

@ -104,7 +104,7 @@
"delete_rule": "删除规则",
"delete_tag": "删除标签",
"delete_tag_description": "您确定要删除这个标签吗?这不能被撤销,打过标签的文件将会被取消链接。",
"delete_warning": "警告:这将永久删除您的{{type}},我们目前还没有回收站…",
"delete_warning": "这将永久删除您的{{type}},我们目前还没有回收站…",
"description": "描述",
"deselect": "取消选择",
"details": "详情",

View file

@ -104,7 +104,7 @@
"delete_rule": "刪除規則",
"delete_tag": "刪除標籤",
"delete_tag_description": "您確定要刪除這個標籤嗎?這不能撤銷,並且帶有標籤的文件將被取消鏈接。",
"delete_warning": "警告:這將永遠刪除您的{{type}},我們還沒有垃圾箱...",
"delete_warning": "這將永遠刪除您的{{type}},我們還沒有垃圾箱...",
"description": "描述",
"deselect": "取消選擇",
"details": "詳情",

View file

@ -73,6 +73,7 @@ export type Procedures = {
{ 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.moveToTrash", 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 } |
@ -81,6 +82,7 @@ export type Procedures = {
{ key: "files.cutFiles", input: LibraryArgs<OldFileCutterJobInit>, result: null } |
{ key: "files.deleteFiles", input: LibraryArgs<OldFileDeleterJobInit>, result: null } |
{ key: "files.eraseFiles", input: LibraryArgs<OldFileEraserJobInit>, result: null } |
{ key: "files.moveToTrash", input: LibraryArgs<OldFileDeleterJobInit>, result: null } |
{ key: "files.removeAccessTime", input: LibraryArgs<number[]>, result: null } |
{ key: "files.renameFile", input: LibraryArgs<RenameFileArgs>, result: null } |
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |

View file

@ -121,7 +121,9 @@ export interface DialogProps<S extends FieldValues>
loading?: boolean;
trigger?: ReactNode;
ctaLabel?: string;
ctaSecondLabel?: string;
onSubmit?: ReturnType<UseFormHandleSubmit<S>>;
onSubmitSecond?: ReturnType<UseFormHandleSubmit<S>>;
children?: ReactNode;
ctaDanger?: boolean;
closeLabel?: string;
@ -141,6 +143,7 @@ export function Dialog<S extends FieldValues>({
form,
dialog,
onSubmit,
onSubmitSecond,
onCancelled = true,
invertButtonFocus,
...props
@ -190,7 +193,7 @@ export function Dialog<S extends FieldValues>({
)
: !form.formState.isValid;
const submitButton = (
const submitButton = !props.ctaSecondLabel ? (
<Button
type="submit"
size="sm"
@ -200,9 +203,54 @@ export function Dialog<S extends FieldValues>({
props.ctaDanger &&
'border-red-500 bg-red-500 focus:ring-1 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-app-selected'
)}
onClick={async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
await onSubmit?.(e);
dialog.onSubmit?.();
setOpen(false);
}}
>
{props.ctaLabel}
</Button>
) : (
<div className="flex flex-row gap-x-2">
<Button
type="submit"
size="sm"
disabled={form.formState.isSubmitting || props.submitDisabled || disableCheck}
variant={props.ctaDanger ? 'colored' : 'accent'}
className={clsx(
props.ctaDanger &&
'border-red-500 bg-red-500 focus:ring-1 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-app-selected'
)}
onClick={async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
await onSubmit?.(e);
dialog.onSubmit?.();
setOpen(false);
}}
>
{props.ctaLabel}
</Button>
<Button
type="submit"
size="sm"
disabled={form.formState.isSubmitting || props.submitDisabled || disableCheck}
variant={props.ctaDanger ? 'colored' : 'accent'}
className={clsx(
props.ctaDanger &&
'border-primary-500 bg-primary-500 focus:ring-1 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-app-selected'
)}
onClick={async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
await onSubmitSecond?.(e);
dialog.onSubmit?.();
setOpen(false);
}}
>
{props.ctaSecondLabel}
</Button>
</div>
);
return (
@ -226,8 +274,6 @@ export function Dialog<S extends FieldValues>({
form={form}
onSubmit={async (e) => {
e?.preventDefault();
await onSubmit?.(e);
dialog.onSubmit?.();
setOpen(false);
}}
className={clsx(