Rename workspace

This commit is contained in:
Gregory Schier
2023-04-09 12:23:41 -07:00
parent 37f0eca79f
commit b82798bf49
20 changed files with 275 additions and 37 deletions

View File

@@ -45,13 +45,20 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
return (
<DialogContext.Provider value={state}>
{children}
{dialogs.map(({ id, render, ...props }) => (
<Dialog open key={id} onClose={() => actions.hide(id)} {...props}>
{render({ hide: () => actions.hide(id) })}
</Dialog>
{dialogs.map((props: DialogEntry) => (
<DialogInstance key={props.id} {...props} />
))}
</DialogContext.Provider>
);
};
function DialogInstance({ id, render, ...props }: DialogEntry) {
const { actions } = useContext(DialogContext);
return (
<Dialog open onClose={() => actions.hide(id)} {...props}>
{render({ hide: () => actions.hide(id) })}
</Dialog>
);
}
export const useDialog = () => useContext(DialogContext).actions;

View File

@@ -111,7 +111,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
{activeResponse && (
<HStack alignItems="center" className="w-full">
<div className="whitespace-nowrap px-3">
<StatusTag response={activeResponse} />
<StatusTag showReason response={activeResponse} />
{activeResponse.elapsed > 0 && <>&nbsp;&bull;&nbsp;{activeResponse.elapsed}ms</>}
{activeResponse.body.length > 0 && (
<>&nbsp;&bull;&nbsp;{(activeResponse.body.length / 1000).toFixed(1)} KB</>
@@ -165,7 +165,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
</TabContent>
<TabContent value="body">
{!activeResponse.body ? (
<EmptyStateText>No Response</EmptyStateText>
<EmptyStateText>Empty Body</EmptyStateText>
) : viewMode === 'pretty' && contentType.includes('html') ? (
<Webview
body={activeResponse.body}

View File

@@ -3,12 +3,15 @@ import { memo, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { usePrompt } from '../hooks/usePrompt';
import { useRoutes } from '../hooks/useRoutes';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
type Props = {
className?: string;
@@ -19,7 +22,9 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = activeWorkspace?.id ?? null;
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
const prompt = usePrompt();
const routes = useRoutes();
const items: DropdownItem[] = useMemo(() => {
@@ -32,31 +37,59 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
},
}));
const activeWorkspaceItems: DropdownItem[] =
workspaces.length <= 1
? []
: [
...workspaceItems,
{
type: 'separator',
label: activeWorkspace?.name,
},
];
return [
...workspaceItems,
...activeWorkspaceItems,
{
type: 'separator',
label: 'Actions',
label: 'Rename',
leftSlot: <Icon icon="pencil" />,
onSelect: async () => {
const name = await prompt({
title: 'Rename Workspace',
description: (
<>
Enter a new name for <InlineCode>{activeWorkspace?.name}</InlineCode>
</>
),
name: 'name',
label: 'Name',
defaultValue: activeWorkspace?.name,
});
updateWorkspace.mutate({ name });
},
},
{
label: 'New Workspace',
leftSlot: <Icon icon="plus" />,
onSelect: () => createWorkspace.mutate({ name: 'New Workspace' }),
},
{
label: 'Delete Workspace',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
onSelect: deleteWorkspace.mutate,
variant: 'danger',
},
{ type: 'separator' },
{
label: 'Create Workspace',
leftSlot: <Icon icon="plus" />,
onSelect: () => createWorkspace.mutate({ name: 'Workspace' }),
},
];
}, [
workspaces,
deleteWorkspace.mutate,
activeWorkspaceId,
routes,
createWorkspace,
confirm,
prompt,
activeWorkspace?.name,
deleteWorkspace,
updateWorkspace,
createWorkspace,
]);
return (

View File

@@ -36,6 +36,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
children,
forDropdown,
color,
type = 'button',
justify = 'center',
size = 'md',
...props
@@ -68,7 +69,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
);
} else {
return (
<button ref={ref} className={classes} {...props}>
<button ref={ref} type={type} className={classes} {...props}>
{isLoading && <Icon icon="update" size={size} className="animate-spin mr-1" />}
{children}
{forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />}

View File

@@ -76,7 +76,7 @@ export function Dialog({
{title}
</Heading>
{description && <p id={descriptionId}>{description}</p>}
<div className="mt-6">{children}</div>
<div className="mt-4">{children}</div>
</motion.div>
</div>
</div>

View File

@@ -2,7 +2,15 @@ import classnames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { motion } from 'framer-motion';
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
import { Children, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
Children,
cloneElement,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useKeyPressEvent } from 'react-use';
import { Portal } from '../Portal';
import { Separator } from './Separator';
@@ -17,6 +25,7 @@ export type DropdownItem =
| {
type?: 'default';
label: string;
variant?: 'danger';
disabled?: boolean;
hidden?: boolean;
leftSlot?: ReactNode;
@@ -94,6 +103,17 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
setMenuStyles({ maxHeight: windowBox.height - menuBox.top - 5 });
}, []);
// Close menu on space bar
const handleMenuKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === ' ') {
e.preventDefault();
onClose();
}
},
[onClose],
);
useKeyPressEvent('Escape', (e) => {
e.preventDefault();
onClose();
@@ -181,6 +201,7 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
<div tabIndex={-1} aria-hidden className="fixed inset-0" onClick={onClose} />
<motion.div
tabIndex={0}
onKeyDown={handleMenuKeyDown}
initial={{ opacity: 0, y: -5, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
role="menu"
@@ -268,6 +289,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
className,
'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap',
'focus:bg-highlight focus:text-gray-900 rounded',
item.variant === 'danger' && 'text-red-600',
)}
{...props}
>

View File

@@ -23,6 +23,7 @@ import {
MagnifyingGlassIcon,
MoonIcon,
PaperPlaneIcon,
Pencil2Icon,
PlusCircledIcon,
PlusIcon,
QuestionMarkIcon,
@@ -65,6 +66,7 @@ const icons = {
magnifyingGlass: MagnifyingGlassIcon,
moon: MoonIcon,
paperPlane: PaperPlaneIcon,
pencil: Pencil2Icon,
plus: PlusIcon,
plusCircle: PlusCircledIcon,
question: QuestionMarkIcon,

View File

@@ -2,11 +2,12 @@ import classnames from 'classnames';
import type { HttpResponse } from '../../lib/models';
interface Props {
response: Pick<HttpResponse, 'status' | 'error'>;
response: Pick<HttpResponse, 'status' | 'statusReason' | 'error'>;
className?: string;
showReason?: boolean;
}
export function StatusTag({ response, className }: Props) {
export function StatusTag({ response, className, showReason }: Props) {
const { status, error } = response;
const label = error ? 'ERR' : status;
return (
@@ -22,7 +23,7 @@ export function StatusTag({ response, className }: Props) {
status >= 500 && 'text-red-600',
)}
>
{label}
{label} {showReason && response.statusReason && response.statusReason}
</span>
);
}

View File

@@ -73,17 +73,17 @@ export function Tabs({
aria-label={label}
className={classnames(
tabListClassName,
'flex items-center overflow-x-auto hide-scrollbars mt-1 mb-2',
'flex items-center overflow-x-auto overflow-y-visible hide-scrollbars mt-1 mb-2',
// Give space for button focus states within overflow boundary.
'px-2 -mx-2',
'-mx-5 pl-3 py-1',
)}
>
<HStack space={1} className="flex-shrink-0">
<HStack space={2} className="flex-shrink-0">
{tabs.map((t) => {
const isActive = t.value === value;
const btnClassName = classnames(
isActive ? '' : 'text-gray-600 hover:text-gray-800',
'!px-0 mr-4 ml-[1px]',
'!px-2 ml-[1px]',
);
if ('options' in t) {