diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6bd1eb1e..5bb87811 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -55,9 +55,9 @@ use yaak_models::queries::{ delete_all_http_responses_for_request, delete_all_http_responses_for_workspace, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_plugin, - delete_workspace, duplicate_grpc_request, duplicate_http_request, generate_id, - generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection, - get_grpc_request, get_http_request, get_http_response, get_key_value_raw, + delete_workspace, duplicate_folder, duplicate_grpc_request, duplicate_http_request, + generate_id, generate_model_id, get_cookie_jar, get_environment, get_folder, + get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments, list_folders, list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses_for_request, list_http_responses_for_workspace, @@ -1242,6 +1242,15 @@ async fn cmd_duplicate_grpc_request(id: &str, w: WebviewWindow) -> Result( + window: WebviewWindow, + id: &str, +) -> Result<(), String> { + let folder = get_folder(&window, id).await.map_err(|e| e.to_string())?; + duplicate_folder(&window, &folder).await.map_err(|e| e.to_string()) +} + #[tauri::command] async fn cmd_create_http_request( request: HttpRequest, @@ -1738,6 +1747,7 @@ pub fn run() { cmd_delete_send_history, cmd_delete_workspace, cmd_dismiss_notification, + cmd_duplicate_folder, cmd_duplicate_grpc_request, cmd_duplicate_http_request, cmd_export_data, diff --git a/src-tauri/yaak_models/src/queries.rs b/src-tauri/yaak_models/src/queries.rs index 4128d49a..75eb5006 100644 --- a/src-tauri/yaak_models/src/queries.rs +++ b/src-tauri/yaak_models/src/queries.rs @@ -306,6 +306,7 @@ pub async fn duplicate_grpc_request( return Err(ModelNotFound(id.to_string())); } }; + request.sort_priority = request.sort_priority + 0.001; request.id = "".to_string(); upsert_grpc_request(window, request).await } @@ -1108,9 +1109,79 @@ pub async fn duplicate_http_request( Some(r) => r, }; request.id = "".to_string(); + request.sort_priority = request.sort_priority + 0.001; upsert_http_request(window, request).await } +pub async fn duplicate_folder( + window: &WebviewWindow, + src_folder: &Folder, +) -> Result<()> { + let workspace_id = src_folder.workspace_id.as_str(); + + let http_requests = list_http_requests(window, workspace_id) + .await? + .into_iter() + .filter(|m| m.folder_id.as_ref() == Some(&src_folder.id)); + + let grpc_requests = list_grpc_requests(window, workspace_id) + .await? + .into_iter() + .filter(|m| m.folder_id.as_ref() == Some(&src_folder.id)); + + let folders = list_folders(window, workspace_id) + .await? + .into_iter() + .filter(|m| m.folder_id.as_ref() == Some(&src_folder.id)); + + let new_folder = upsert_folder( + window, + Folder { + id: "".into(), + sort_priority: src_folder.sort_priority + 0.001, + ..src_folder.clone() + }, + ) + .await?; + + for m in http_requests { + upsert_http_request( + window, + HttpRequest { + id: "".into(), + folder_id: Some(new_folder.id.clone()), + sort_priority: m.sort_priority + 0.001, + ..m + }, + ) + .await?; + } + for m in grpc_requests { + upsert_grpc_request( + window, + GrpcRequest { + id: "".into(), + folder_id: Some(new_folder.id.clone()), + sort_priority: m.sort_priority + 0.001, + ..m + }, + ) + .await?; + } + for m in folders { + // Recurse down + Box::pin(duplicate_folder( + window, + &Folder { + folder_id: Some(new_folder.id.clone()), + ..m + }, + )) + .await?; + } + Ok(()) +} + pub async fn upsert_http_request( window: &WebviewWindow, r: HttpRequest, diff --git a/src-web/components/ExportDataDialog.tsx b/src-web/components/ExportDataDialog.tsx index 0f4fca26..e28408a0 100644 --- a/src-web/components/ExportDataDialog.tsx +++ b/src-web/components/ExportDataDialog.tsx @@ -64,8 +64,7 @@ export function ExportDataDialog({ - Docs + Info {activeRequest.description && } ), diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 0fdf11ce..4e138032 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -129,7 +129,7 @@ export const RequestPane = memo(function RequestPane({ value: TAB_DESCRIPTION, label: (
- Docs + Info {activeRequest.description && }
), diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 2ab5c68e..0d92dbcb 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -22,6 +22,7 @@ import { useAppRoutes } from '../hooks/useAppRoutes'; import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems'; import { useDeleteFolder } from '../hooks/useDeleteFolder'; import { useDeleteRequest } from '../hooks/useDeleteRequest'; +import { useDuplicateFolder } from '../hooks/useDuplicateFolder'; import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest'; import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest'; import { useFolders } from '../hooks/useFolders'; @@ -699,6 +700,7 @@ function SidebarItem({ const deleteFolder = useDeleteFolder(itemId); const deleteRequest = useDeleteRequest(itemId); const renameRequest = useRenameRequest(itemId); + const duplicateFolder = useDuplicateFolder(itemId); const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true }); const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true }); const sendRequest = useSendAnyHttpRequest(); @@ -802,6 +804,12 @@ function SidebarItem({ render: () => , }), }, + { + key: 'duplicateFolder', + label: 'Duplicate', + leftSlot: , + onSelect: () => duplicateFolder.mutate(), + }, { key: 'delete-folder', label: 'Delete', @@ -877,7 +885,7 @@ function SidebarItem({ createDropdownItems, deleteFolder, deleteRequest, - dialog, + duplicateFolder, duplicateGrpcRequest, duplicateHttpRequest, httpRequestActions, diff --git a/src-web/hooks/useDuplicateFolder.ts b/src-web/hooks/useDuplicateFolder.ts new file mode 100644 index 00000000..31facdd0 --- /dev/null +++ b/src-web/hooks/useDuplicateFolder.ts @@ -0,0 +1,11 @@ +import { useMutation } from '@tanstack/react-query'; +import { trackEvent } from '../lib/analytics'; +import { invokeCmd } from '../lib/tauri'; + +export function useDuplicateFolder(id: string) { + return useMutation({ + mutationKey: ['duplicate_folder', id], + mutationFn: () => invokeCmd('cmd_duplicate_folder', { id }), + onSettled: () => trackEvent('folder', 'duplicate'), + }); +} diff --git a/src-web/lib/tauri.ts b/src-web/lib/tauri.ts index 466d12c8..82f99c3d 100644 --- a/src-web/lib/tauri.ts +++ b/src-web/lib/tauri.ts @@ -24,6 +24,7 @@ type TauriCmd = | 'cmd_delete_http_response' | 'cmd_delete_workspace' | 'cmd_dismiss_notification' + | 'cmd_duplicate_folder' | 'cmd_duplicate_grpc_request' | 'cmd_duplicate_http_request' | 'cmd_export_data'