Folder actions

This commit is contained in:
Gregory Schier
2023-11-04 10:48:18 -07:00
parent 9471009b8b
commit de190ca8fa
7 changed files with 163 additions and 18 deletions

View File

@@ -405,6 +405,7 @@ async fn create_request(
workspace_id: &str, workspace_id: &str,
name: &str, name: &str,
sort_priority: f64, sort_priority: f64,
folder_id: Option<&str>,
window: Window<Wry>, window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpRequest, String> { ) -> Result<models::HttpRequest, String> {
@@ -415,6 +416,7 @@ async fn create_request(
workspace_id: workspace_id.to_string(), workspace_id: workspace_id.to_string(),
name: name.to_string(), name: name.to_string(),
method: "GET".to_string(), method: "GET".to_string(),
folder_id: folder_id.map(|s| s.to_string()),
sort_priority, sort_priority,
..Default::default() ..Default::default()
}, },

View File

@@ -8,8 +8,10 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequestId } from '../hooks/useActiveRequestId'; import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateFolder } from '../hooks/useCreateFolder';
import { useCreateRequest } from '../hooks/useCreateRequest'; import { useCreateRequest } from '../hooks/useCreateRequest';
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest'; import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useFolders } from '../hooks/useFolders'; import { useFolders } from '../hooks/useFolders';
import { useLatestResponse } from '../hooks/useLatestResponse'; import { useLatestResponse } from '../hooks/useLatestResponse';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
@@ -20,7 +22,9 @@ import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { Folder, HttpRequest, Workspace } from '../lib/models'; import type { Folder, HttpRequest, Workspace } from '../lib/models';
import { isResponseLoading } from '../lib/models'; import { isResponseLoading } from '../lib/models';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag'; import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker'; import { DropMarker } from './DropMarker';
@@ -386,7 +390,17 @@ function SidebarItems({
handleDragStart, handleDragStart,
}: SidebarItemsProps) { }: SidebarItemsProps) {
return ( return (
<VStack as="ul" role="menu" aria-orientation="vertical" dir="ltr"> <VStack
as="ul"
role="menu"
aria-orientation="vertical"
dir="ltr"
className={classNames(
tree.depth > 0 && 'border-l border-highlight',
tree.depth === 0 && 'ml-0',
tree.depth >= 1 && 'ml-[1.3em]',
)}
>
{tree.children.map((child, i) => ( {tree.children.map((child, i) => (
<Fragment key={child.item.id}> <Fragment key={child.item.id}>
{hoveredIndex === i && hoveredTree?.item.id === tree.item.id && ( {hoveredIndex === i && hoveredTree?.item.id === tree.item.id && (
@@ -403,7 +417,6 @@ function SidebarItems({
onDragStart={handleDragStart} onDragStart={handleDragStart}
useProminentStyles={focused} useProminentStyles={focused}
collapsed={collapsed} collapsed={collapsed}
className={classNames(tree.depth > 0 && 'border-l border-highlight ml-5')}
> >
{child.item.model === 'folder' && {child.item.model === 'folder' &&
!collapsed[child.item.id] && !collapsed[child.item.id] &&
@@ -461,6 +474,9 @@ const SidebarItem = forwardRef(function SidebarItem(
}: SidebarItemProps, }: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>, ref: ForwardedRef<HTMLLIElement>,
) { ) {
const createRequest = useCreateRequest();
const createFolder = useCreateFolder();
const deleteRequest = useDeleteFolder(itemId);
const latestResponse = useLatestResponse(itemId); const latestResponse = useLatestResponse(itemId);
const updateRequest = useUpdateRequest(itemId); const updateRequest = useUpdateRequest(itemId);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
@@ -515,7 +531,37 @@ const SidebarItem = forwardRef(function SidebarItem(
return ( return (
<li ref={ref}> <li ref={ref}>
<div className={classNames(className, 'block group/item px-2 pb-0.5')}> <div className={classNames(className, 'block relative group/item px-2 pb-0.5')}>
{itemModel === 'folder' && (
<Dropdown
items={[
{
key: 'createRequest',
label: 'New Request',
onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
},
{
key: 'createFolder',
label: 'New Folder',
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
},
{ type: 'separator' },
{
key: 'deleteFolder',
label: 'Delete',
variant: 'danger',
onSelect: () => deleteRequest.mutate(),
},
]}
>
<IconButton
title="Folder options"
size="xs"
icon="dotsV"
className="ml-auto !bg-transparent absolute right-2 opacity-20 group-hover/item:opacity-70 transition-opacity"
/>
</Dropdown>
)}
<button <button
// tabIndex={-1} // Will prevent drag-n-drop // tabIndex={-1} // Will prevent drag-n-drop
onClick={handleSelect} onClick={handleSelect}
@@ -535,8 +581,11 @@ const SidebarItem = forwardRef(function SidebarItem(
{itemModel === 'folder' && ( {itemModel === 'folder' && (
<Icon <Icon
size="sm" size="sm"
icon={collapsed[itemId] ? 'chevronRight' : 'chevronDown'} icon="chevronRight"
className="-ml-0.5 mr-2" className={classNames(
'-ml-0.5 mr-2 transition-transform',
!collapsed[itemId] && 'transform rotate-90',
)}
/> />
)} )}
{editing ? ( {editing ? (

View File

@@ -1,11 +1,14 @@
import { memo } from 'react'; import { memo } from 'react';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useCreateFolder } from '../hooks/useCreateFolder';
import { IconButton } from './core/IconButton';
import { useCreateRequest } from '../hooks/useCreateRequest'; import { useCreateRequest } from '../hooks/useCreateRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { Dropdown } from './core/Dropdown';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
export const SidebarActions = memo(function SidebarActions() { export const SidebarActions = memo(function SidebarActions() {
const createRequest = useCreateRequest(); const createRequest = useCreateRequest();
const createFolder = useCreateFolder();
const { hidden, toggle } = useSidebarHidden(); const { hidden, toggle } = useSidebarHidden();
return ( return (
@@ -19,12 +22,22 @@ export const SidebarActions = memo(function SidebarActions() {
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'} icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
/> />
)} )}
<IconButton <Dropdown
size="sm" items={[
icon="plusCircle" {
title="Create Request" key: 'create-request',
onClick={() => createRequest.mutate({})} label: 'Create Request',
/> onSelect: () => createRequest.mutate({}),
},
{
key: 'create-folder',
label: 'Create Folder',
onSelect: () => createFolder.mutate({}),
},
]}
>
<IconButton size="sm" icon="plusCircle" title="Add Resource" />
</Dropdown>
</HStack> </HStack>
); );
}); });

View File

@@ -1,6 +1,13 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react'; import type {
CSSProperties,
FocusEvent as ReactFocusEvent,
HTMLAttributes,
MouseEvent,
ReactElement,
ReactNode,
} from 'react';
import React, { import React, {
Children, Children,
cloneElement, cloneElement,
@@ -13,10 +20,10 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use'; import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
import { Overlay } from '../Overlay';
import { Button } from './Button'; import { Button } from './Button';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { VStack } from './Stacks'; import { VStack } from './Stacks';
import { Overlay } from '../Overlay';
export type DropdownItemSeparator = { export type DropdownItemSeparator = {
type: 'separator'; type: 'separator';
@@ -334,7 +341,13 @@ interface MenuItemProps {
function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: MenuItemProps) { function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: MenuItemProps) {
const handleClick = useCallback(() => onSelect?.(item), [item, onSelect]); const handleClick = useCallback(() => onSelect?.(item), [item, onSelect]);
const handleFocus = useCallback(() => onFocus?.(item), [item, onFocus]); const handleFocus = useCallback(
(e: ReactFocusEvent<HTMLButtonElement>) => {
e.stopPropagation(); // Don't trigger focus on any parents
return onFocus?.(item);
},
[item, onFocus],
);
const initRef = useCallback( const initRef = useCallback(
(el: HTMLButtonElement | null) => { (el: HTMLButtonElement | null) => {

View File

@@ -0,0 +1,24 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { Folder } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { foldersQueryKey } from './useFolders';
export function useCreateFolder() {
const workspaceId = useActiveWorkspaceId();
const queryClient = useQueryClient();
return useMutation<Folder, unknown, Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>>({
mutationFn: (patch) => {
if (workspaceId === null) {
throw new Error("Cannot create folder when there's no active workspace");
}
patch.name = patch.name || 'New Folder';
patch.sortPriority = patch.sortPriority || Date.now();
return invoke('create_folder', { workspaceId, ...patch });
},
onSuccess: async (request) => {
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId }));
},
});
}

View File

@@ -1,10 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes'; import { useAppRoutes } from './useAppRoutes';
import { requestsQueryKey, useRequests } from './useRequests'; import { requestsQueryKey, useRequests } from './useRequests';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
export function useCreateRequest() { export function useCreateRequest() {
const workspaceId = useActiveWorkspaceId(); const workspaceId = useActiveWorkspaceId();
@@ -13,7 +13,11 @@ export function useCreateRequest() {
const requests = useRequests(); const requests = useRequests();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<HttpRequest, unknown, Partial<Pick<HttpRequest, 'name' | 'sortPriority'>>>({ return useMutation<
HttpRequest,
unknown,
Partial<Pick<HttpRequest, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationFn: (patch) => { mutationFn: (patch) => {
if (workspaceId === null) { if (workspaceId === null) {
throw new Error("Cannot create request when there's no active workspace"); throw new Error("Cannot create request when there's no active workspace");

View File

@@ -0,0 +1,40 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { InlineCode } from '../components/core/InlineCode';
import type { Folder } from '../lib/models';
import { getFolder } from '../lib/store';
import { useConfirm } from './useConfirm';
import { foldersQueryKey } from './useFolders';
import { requestsQueryKey } from './useRequests';
export function useDeleteFolder(id: string | null) {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<Folder | null, string>({
mutationFn: async () => {
const folder = await getFolder(id);
const confirmed = await confirm({
title: 'Delete Folder',
variant: 'delete',
description: (
<>
Permanently delete <InlineCode>{folder?.name}</InlineCode> and everything in it?
</>
),
});
if (!confirmed) return null;
return invoke('delete_folder', { folderId: id });
},
onSuccess: async (folder) => {
// Was it cancelled?
if (folder === null) return;
const { workspaceId } = folder;
// Nesting makes it hard to clean things up, so just clear everything that could have been deleted
await queryClient.invalidateQueries(requestsQueryKey({ workspaceId }));
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId }));
},
});
}