Folder actions

This commit is contained in:
Gregory Schier
2023-11-04 10:48:18 -07:00
parent 5aed4b79be
commit 7755d06bba
7 changed files with 163 additions and 18 deletions

View File

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

View File

@@ -8,8 +8,10 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateFolder } from '../hooks/useCreateFolder';
import { useCreateRequest } from '../hooks/useCreateRequest';
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useFolders } from '../hooks/useFolders';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
@@ -20,7 +22,9 @@ import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { Folder, HttpRequest, Workspace } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker';
@@ -386,7 +390,17 @@ function SidebarItems({
handleDragStart,
}: SidebarItemsProps) {
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) => (
<Fragment key={child.item.id}>
{hoveredIndex === i && hoveredTree?.item.id === tree.item.id && (
@@ -403,7 +417,6 @@ function SidebarItems({
onDragStart={handleDragStart}
useProminentStyles={focused}
collapsed={collapsed}
className={classNames(tree.depth > 0 && 'border-l border-highlight ml-5')}
>
{child.item.model === 'folder' &&
!collapsed[child.item.id] &&
@@ -461,6 +474,9 @@ const SidebarItem = forwardRef(function SidebarItem(
}: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>,
) {
const createRequest = useCreateRequest();
const createFolder = useCreateFolder();
const deleteRequest = useDeleteFolder(itemId);
const latestResponse = useLatestResponse(itemId);
const updateRequest = useUpdateRequest(itemId);
const [editing, setEditing] = useState<boolean>(false);
@@ -515,7 +531,37 @@ const SidebarItem = forwardRef(function SidebarItem(
return (
<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
// tabIndex={-1} // Will prevent drag-n-drop
onClick={handleSelect}
@@ -535,8 +581,11 @@ const SidebarItem = forwardRef(function SidebarItem(
{itemModel === 'folder' && (
<Icon
size="sm"
icon={collapsed[itemId] ? 'chevronRight' : 'chevronDown'}
className="-ml-0.5 mr-2"
icon="chevronRight"
className={classNames(
'-ml-0.5 mr-2 transition-transform',
!collapsed[itemId] && 'transform rotate-90',
)}
/>
)}
{editing ? (

View File

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

View File

@@ -1,6 +1,13 @@
import classNames from 'classnames';
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, {
Children,
cloneElement,
@@ -13,10 +20,10 @@ import React, {
useState,
} from 'react';
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
import { Overlay } from '../Overlay';
import { Button } from './Button';
import { Separator } from './Separator';
import { VStack } from './Stacks';
import { Overlay } from '../Overlay';
export type DropdownItemSeparator = {
type: 'separator';
@@ -334,7 +341,13 @@ interface MenuItemProps {
function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: MenuItemProps) {
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(
(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 { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { requestsQueryKey, useRequests } from './useRequests';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
export function useCreateRequest() {
const workspaceId = useActiveWorkspaceId();
@@ -13,7 +13,11 @@ export function useCreateRequest() {
const requests = useRequests();
const queryClient = useQueryClient();
return useMutation<HttpRequest, unknown, Partial<Pick<HttpRequest, 'name' | 'sortPriority'>>>({
return useMutation<
HttpRequest,
unknown,
Partial<Pick<HttpRequest, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationFn: (patch) => {
if (workspaceId === null) {
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 }));
},
});
}