Move request to another workspace (#52)

This commit is contained in:
Gregory Schier
2024-06-21 09:01:18 -07:00
committed by GitHub
parent dc5dfeb022
commit 50e2ab3a03
19 changed files with 253 additions and 77 deletions

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_requests (\n id, workspace_id, folder_id, name, url, url_parameters, method, body, body_type,\n authentication, authentication_type, headers, sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "11394af12419cca3be3a26dff9275514ea2a44504e3c7a568a9578c64b5713d1"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_requests (\n id, name, workspace_id, folder_id, sort_priority, url, service, method, message,\n authentication_type, authentication, metadata\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority,\n url = excluded.url,\n service = excluded.service,\n method = excluded.method,\n message = excluded.message,\n authentication_type = excluded.authentication_type,\n authentication = excluded.authentication,\n metadata = excluded.metadata\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "467b87ad1209a4653b1dc8462d79236a655240c5b402fa9fd75c12ebd9bb6b86"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_requests (\n id, name, workspace_id, folder_id, sort_priority, url, service, method, message,\n authentication_type, authentication, metadata\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n workspace_id = excluded.workspace_id,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority,\n url = excluded.url,\n service = excluded.service,\n method = excluded.method,\n message = excluded.message,\n authentication_type = excluded.authentication_type,\n authentication = excluded.authentication,\n metadata = excluded.metadata\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "5af82cd333895d3d7d67a92f37b0feb338f615b88aea2bd09cb5809008c645a3"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_requests (\n id, workspace_id, folder_id, name, url, url_parameters, method, body, body_type,\n authentication, authentication_type, headers, sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n workspace_id = excluded.workspace_id,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "5f2f40062abbe93e23b38876319cf16d4d2b3f8d0be32ffe7848528c725e1429"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance,\n theme_dark, theme_light, update_channel,\n interface_font_size, interface_scale, editor_font_size, editor_soft_wrap, \n open_workspace_new_window\n FROM settings\n WHERE id = 'default'\n ", "query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance,\n theme_dark, theme_light, update_channel,\n interface_font_size, interface_scale, editor_font_size, editor_soft_wrap,\n open_workspace_new_window\n FROM settings\n WHERE id = 'default'\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -94,5 +94,5 @@
true true
] ]
}, },
"hash": "05dca7fe15ab1bf03952e94498ef3130e16f752da72782783696eb2cca4736d5" "hash": "daa61066517df649e7c80a8ce407839ad502e8e5e43aa8c02e049865acbbae75"
} }

View File

@@ -562,6 +562,7 @@ pub async fn upsert_grpc_request(
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP,
workspace_id = excluded.workspace_id,
name = excluded.name, name = excluded.name,
folder_id = excluded.folder_id, folder_id = excluded.folder_id,
sort_priority = excluded.sort_priority, sort_priority = excluded.sort_priority,
@@ -892,7 +893,7 @@ async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings, sqlx::Error>
SELECT SELECT
id, model, created_at, updated_at, theme, appearance, id, model, created_at, updated_at, theme, appearance,
theme_dark, theme_light, update_channel, theme_dark, theme_light, update_channel,
interface_font_size, interface_scale, editor_font_size, editor_soft_wrap, interface_font_size, interface_scale, editor_font_size, editor_soft_wrap,
open_workspace_new_window open_workspace_new_window
FROM settings FROM settings
WHERE id = 'default' WHERE id = 'default'
@@ -1125,6 +1126,7 @@ pub async fn upsert_http_request(
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP,
workspace_id = excluded.workspace_id,
name = excluded.name, name = excluded.name,
folder_id = excluded.folder_id, folder_id = excluded.folder_id,
method = excluded.method, method = excluded.method,

View File

@@ -1,16 +1,21 @@
import { Outlet } from 'react-router-dom';
import { DialogProvider } from './DialogContext';
import { GlobalHooks } from './GlobalHooks';
import { ToastProvider } from './ToastContext';
import classNames from 'classnames'; import classNames from 'classnames';
import { useOsInfo } from '../hooks/useOsInfo';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Outlet } from 'react-router-dom';
import { useOsInfo } from '../hooks/useOsInfo';
import { DialogProvider, Dialogs } from './DialogContext';
import { GlobalHooks } from './GlobalHooks';
import { ToastProvider, Toasts } from './ToastContext';
export function DefaultLayout() { export function DefaultLayout() {
const osInfo = useOsInfo(); const osInfo = useOsInfo();
return ( return (
<DialogProvider> <DialogProvider>
<ToastProvider> <ToastProvider>
<>
{/* Must be inside all the providers, so they have access to them */}
<Toasts />
<Dialogs />
</>
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}

View File

@@ -46,14 +46,7 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
actions, actions,
}; };
return ( return <DialogContext.Provider value={state}>{children}</DialogContext.Provider>;
<DialogContext.Provider value={state}>
{children}
{dialogs.map((props: DialogEntry) => (
<DialogInstance key={props.id} {...props} />
))}
</DialogContext.Provider>
);
}; };
function DialogInstance({ id, render, ...props }: DialogEntry) { function DialogInstance({ id, render, ...props }: DialogEntry) {
@@ -67,3 +60,14 @@ function DialogInstance({ id, render, ...props }: DialogEntry) {
} }
export const useDialog = () => useContext(DialogContext).actions; export const useDialog = () => useContext(DialogContext).actions;
export function Dialogs() {
const { dialogs } = useContext(DialogContext);
return (
<>
{dialogs.map((props: DialogEntry) => (
<DialogInstance key={props.id} {...props} />
))}
</>
);
}

View File

@@ -1,6 +1,7 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { getCurrent } from '@tauri-apps/api/webviewWindow'; import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useClipboardText } from '../hooks/useClipboardText'; import { useClipboardText } from '../hooks/useClipboardText';
import { useCommandPalette } from '../hooks/useCommandPalette'; import { useCommandPalette } from '../hooks/useCommandPalette';
import { cookieJarsQueryKey } from '../hooks/useCookieJars'; import { cookieJarsQueryKey } from '../hooks/useCookieJars';
@@ -32,8 +33,12 @@ import { monokaiProDefault } from '../lib/theme/themes/monokai-pro';
import { rosePineDefault } from '../lib/theme/themes/rose-pine'; import { rosePineDefault } from '../lib/theme/themes/rose-pine';
import { yaakDark } from '../lib/theme/themes/yaak'; import { yaakDark } from '../lib/theme/themes/yaak';
import { getThemeCSS } from '../lib/theme/window'; import { getThemeCSS } from '../lib/theme/window';
import { InlineCode } from './core/InlineCode';
import { useToast } from './ToastContext';
export function GlobalHooks() { export function GlobalHooks() {
const toast = useToast();
// Include here so they always update, even if no component references them // Include here so they always update, even if no component references them
useRecentWorkspaces(); useRecentWorkspaces();
useRecentEnvironments(); useRecentEnvironments();
@@ -44,6 +49,21 @@ export function GlobalHooks() {
useCommandPalette(); useCommandPalette();
useNotificationToast(); useNotificationToast();
const activeWorkspace = useActiveWorkspace();
useEffect(() => {
if (activeWorkspace == null) return;
toast.show({
id: 'workspace-changed',
timeout: 3000,
message: (
<>
Switched workspace to <InlineCode>{activeWorkspace.name}</InlineCode>
</>
),
});
}, [activeWorkspace, toast]);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null); const { wasUpdatedExternally } = useRequestUpdateKey(null);

View File

@@ -63,9 +63,9 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
firstSlot={() => firstSlot={() =>
activeConnection && ( activeConnection && (
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center"> <div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
<HStack className="pl-3 mb-1 font-mono"> <HStack className="pl-3 mb-1 font-mono text-sm">
<HStack space={2}> <HStack space={2}>
<span>{events.length} messages</span> <span>{events.length} Messages</span>
{isResponseLoading(activeConnection) && ( {isResponseLoading(activeConnection) && (
<Icon icon="refresh" size="sm" spin className="text-fg-subtler" /> <Icon icon="refresh" size="sm" spin className="text-fg-subtler" />
)} )}

View File

@@ -13,7 +13,7 @@ import { BearerAuth } from './BearerAuth';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { PairEditor } from './core/PairEditor'; import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { RadioDropdown } from './core/RadioDropdown'; import { RadioDropdown } from './core/RadioDropdown';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from './core/Tabs/Tabs';
@@ -209,7 +209,7 @@ export function GrpcConnectionSetupPane({
rightSlot={<Icon className="text-fg-subtler" size="sm" icon="chevronDown" />} rightSlot={<Icon className="text-fg-subtler" size="sm" icon="chevronDown" />}
disabled={isStreaming || services == null} disabled={isStreaming || services == null}
className={classNames( className={classNames(
'font-mono text-sm min-w-[5rem] !ring-0', 'font-mono text-editor min-w-[5rem] !ring-0',
paneSize < 400 && 'flex-1', paneSize < 400 && 'flex-1',
)} )}
> >
@@ -291,7 +291,8 @@ export function GrpcConnectionSetupPane({
)} )}
</TabContent> </TabContent>
<TabContent value="metadata"> <TabContent value="metadata">
<PairEditor <PairOrBulkEditor
preferenceName="grpc_metadata"
valueAutocompleteVariables valueAutocompleteVariables
nameAutocompleteVariables nameAutocompleteVariables
pairs={activeRequest.metadata} pairs={activeRequest.metadata}

View File

@@ -0,0 +1,97 @@
import { useQueryClient } from '@tanstack/react-query';
import React, { useState } from 'react';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { GrpcRequest, HttpRequest } from '../lib/models';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
import { useToast } from './ToastContext';
interface Props {
activeWorkspaceId: string;
request: HttpRequest | GrpcRequest;
onDone: () => void;
}
export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Props) {
const workspaces = useWorkspaces();
const queryClient = useQueryClient();
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const toast = useToast();
const routes = useAppRoutes();
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(activeWorkspaceId);
return (
<VStack space={4} className="mb-4">
<Select
label="Workspace"
name="workspace"
value={selectedWorkspaceId}
onChange={setSelectedWorkspaceId}
options={workspaces.map((w) => ({
label: w.id === activeWorkspaceId ? `${w.name} (current)` : w.name,
value: w.id,
}))}
/>
<Button
color="primary"
disabled={selectedWorkspaceId === activeWorkspaceId}
onClick={async () => {
const args = {
id: request.id,
update: { workspaceId: selectedWorkspaceId, folderId: null },
};
if (request.model === 'http_request') {
await updateHttpRequest.mutateAsync(args);
queryClient.invalidateQueries({
queryKey: httpRequestsQueryKey({ workspaceId: activeWorkspaceId }),
});
} else if (request.model === 'grpc_request') {
await updateGrpcRequest.mutateAsync(args);
queryClient.invalidateQueries({
queryKey: grpcRequestsQueryKey({ workspaceId: activeWorkspaceId }),
});
}
// Hide after a moment, to give time for request to disappear
setTimeout(onDone, 100);
toast.show({
id: 'workspace-moved',
message: (
<>
<InlineCode>{fallbackRequestName(request)}</InlineCode> moved to{' '}
<InlineCode>
{workspaces.find((w) => w.id === selectedWorkspaceId)?.name ?? 'unknown'}
</InlineCode>
</>
),
action: (
<Button
size="xs"
color="secondary"
className="mr-auto min-w-[5rem]"
onClick={() => {
toast.hide('workspace-moved');
routes.navigate('workspace', { workspaceId: selectedWorkspaceId });
}}
>
Switch to Workspace
</Button>
),
});
}}
>
Change Workspace
</Button>
</VStack>
);
}

View File

@@ -20,6 +20,7 @@ import { useHotKey } from '../hooks/useHotKey';
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from '../hooks/useKeyValue';
import { useLatestGrpcConnection } from '../hooks/useLatestGrpcConnection'; import { useLatestGrpcConnection } from '../hooks/useLatestGrpcConnection';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useSendManyRequests } from '../hooks/useSendFolder'; import { useSendManyRequests } from '../hooks/useSendFolder';
@@ -30,6 +31,7 @@ import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '../lib/models'; import type { Folder, GrpcRequest, HttpRequest, Workspace } from '../lib/models';
import { isResponseLoading } from '../lib/models'; import { isResponseLoading } from '../lib/models';
@@ -608,10 +610,12 @@ const SidebarItem = forwardRef(function SidebarItem(
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true }); const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const copyAsCurl = useCopyAsCurl(itemId); const copyAsCurl = useCopyAsCurl(itemId);
const sendRequest = useSendRequest(itemId); const sendRequest = useSendRequest(itemId);
const moveToWorkspace = useMoveToWorkspace(itemId);
const sendManyRequests = useSendManyRequests(); const sendManyRequests = useSendManyRequests();
const latestHttpResponse = useLatestHttpResponse(itemId); const latestHttpResponse = useLatestHttpResponse(itemId);
const latestGrpcConnection = useLatestGrpcConnection(itemId); const latestGrpcConnection = useLatestGrpcConnection(itemId);
const updateHttpRequest = useUpdateHttpRequest(itemId); const updateHttpRequest = useUpdateHttpRequest(itemId);
const workspaces = useWorkspaces();
const updateGrpcRequest = useUpdateGrpcRequest(itemId); const updateGrpcRequest = useUpdateGrpcRequest(itemId);
const updateAnyFolder = useUpdateAnyFolder(); const updateAnyFolder = useUpdateAnyFolder();
const prompt = usePrompt(); const prompt = usePrompt();
@@ -732,7 +736,7 @@ const SidebarItem = forwardRef(function SidebarItem(
hotKeyAction: 'http_request.send', hotKeyAction: 'http_request.send',
hotKeyLabelOnly: true, // Already bound in URL bar hotKeyLabelOnly: true, // Already bound in URL bar
leftSlot: <Icon icon="sendHorizontal" />, leftSlot: <Icon icon="sendHorizontal" />,
onSelect: () => sendRequest.mutate(), onSelect: sendRequest.mutate,
}, },
{ {
key: 'copyCurl', key: 'copyCurl',
@@ -783,6 +787,13 @@ const SidebarItem = forwardRef(function SidebarItem(
: duplicateGrpcRequest.mutate(); : duplicateGrpcRequest.mutate();
}, },
}, },
{
key: 'moveWorkspace',
label: 'Change Workspace',
leftSlot: <Icon icon="house" />,
hidden: workspaces.length <= 1,
onSelect: moveToWorkspace.mutate,
},
{ {
key: 'deleteRequest', key: 'deleteRequest',
variant: 'danger', variant: 'danger',

View File

@@ -9,7 +9,7 @@ import { AnimatePresence } from 'framer-motion';
type ToastEntry = { type ToastEntry = {
id?: string; id?: string;
message: ReactNode; message: ReactNode;
timeout?: number | null; timeout?: 3000 | 5000 | 8000 | null;
onClose?: ToastProps['onClose']; onClose?: ToastProps['onClose'];
} & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>; } & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>;
@@ -29,14 +29,14 @@ interface Actions {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const ToastContext = createContext<State>({} as State); export const ToastContext = createContext<State>({} as State);
export const ToastProvider = ({ children }: { children: React.ReactNode }) => { export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
const [toasts, setToasts] = useState<State['toasts']>([]); const [toasts, setToasts] = useState<State['toasts']>([]);
const timeoutRef = useRef<NodeJS.Timeout>(); const timeoutRef = useRef<NodeJS.Timeout>();
const actions = useMemo<Actions>( const actions = useMemo<Actions>(
() => ({ () => ({
show({ id, timeout = 4000, ...props }: ToastEntry) { show({ id, timeout = 5000, ...props }: ToastEntry) {
id = id ?? generateId(); id = id ?? generateId();
if (timeout != null) { if (timeout != null) {
timeoutRef.current = setTimeout(() => this.hide(id), timeout); timeoutRef.current = setTimeout(() => this.hide(id), timeout);
@@ -62,21 +62,7 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
); );
const state: State = { toasts, actions }; const state: State = { toasts, actions };
return <ToastContext.Provider value={state}>{children}</ToastContext.Provider>;
return (
<ToastContext.Provider value={state}>
{children}
<Portal name="toasts">
<div className="absolute right-0 bottom-0 z-10">
<AnimatePresence>
{toasts.map((props: PrivateToastEntry) => (
<ToastInstance key={props.id} {...props} />
))}
</AnimatePresence>
</div>
</Portal>
</ToastContext.Provider>
);
}; };
function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) { function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) {
@@ -96,3 +82,18 @@ function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) {
} }
export const useToast = () => useContext(ToastContext).actions; export const useToast = () => useContext(ToastContext).actions;
export const Toasts = () => {
const { toasts } = useContext(ToastContext);
return (
<Portal name="toasts">
<div className="absolute right-0 bottom-0 z-10">
<AnimatePresence>
{toasts.map((props: PrivateToastEntry) => (
<ToastInstance key={props.id} {...props} />
))}
</AnimatePresence>
</div>
</Portal>
);
};

View File

@@ -5,9 +5,6 @@ import { memo } from 'react';
const icons = { const icons = {
alert: lucide.AlertTriangleIcon, alert: lucide.AlertTriangleIcon,
text: lucide.FileTextIcon,
table: lucide.TableIcon,
fileCode: lucide.FileCodeIcon,
archive: lucide.ArchiveIcon, archive: lucide.ArchiveIcon,
arrowBigDownDash: lucide.ArrowBigDownDashIcon, arrowBigDownDash: lucide.ArrowBigDownDashIcon,
arrowBigLeftDash: lucide.ArrowBigLeftDashIcon, arrowBigLeftDash: lucide.ArrowBigLeftDashIcon,
@@ -33,19 +30,22 @@ const icons = {
externalLink: lucide.ExternalLinkIcon, externalLink: lucide.ExternalLinkIcon,
eye: lucide.EyeIcon, eye: lucide.EyeIcon,
eyeClosed: lucide.EyeOffIcon, eyeClosed: lucide.EyeOffIcon,
fileCode: lucide.FileCodeIcon,
filter: lucide.FilterIcon, filter: lucide.FilterIcon,
flask: lucide.FlaskConicalIcon, flask: lucide.FlaskConicalIcon,
folderInput: lucide.FolderInputIcon, folderInput: lucide.FolderInputIcon,
folderOutput: lucide.FolderOutputIcon, folderOutput: lucide.FolderOutputIcon,
gripVertical: lucide.GripVerticalIcon, gripVertical: lucide.GripVerticalIcon,
hand: lucide.HandIcon,
house: lucide.HomeIcon,
info: lucide.InfoIcon, info: lucide.InfoIcon,
keyboard: lucide.KeyboardIcon, keyboard: lucide.KeyboardIcon,
leftPanelHidden: lucide.PanelLeftOpenIcon, leftPanelHidden: lucide.PanelLeftOpenIcon,
leftPanelVisible: lucide.PanelLeftCloseIcon, leftPanelVisible: lucide.PanelLeftCloseIcon,
magicWand: lucide.Wand2Icon, magicWand: lucide.Wand2Icon,
minus: lucide.MinusIcon, minus: lucide.MinusIcon,
moreVertical: lucide.MoreVerticalIcon,
moon: lucide.MoonIcon, moon: lucide.MoonIcon,
moreVertical: lucide.MoreVerticalIcon,
paste: lucide.ClipboardPasteIcon, paste: lucide.ClipboardPasteIcon,
pencil: lucide.PencilIcon, pencil: lucide.PencilIcon,
pin: lucide.PinIcon, pin: lucide.PinIcon,
@@ -61,6 +61,8 @@ const icons = {
settings: lucide.SettingsIcon, settings: lucide.SettingsIcon,
sparkles: lucide.SparklesIcon, sparkles: lucide.SparklesIcon,
sun: lucide.SunIcon, sun: lucide.SunIcon,
table: lucide.TableIcon,
text: lucide.FileTextIcon,
trash: lucide.Trash2Icon, trash: lucide.Trash2Icon,
unpin: lucide.PinOffIcon, unpin: lucide.PinOffIcon,
update: lucide.RefreshCcwIcon, update: lucide.RefreshCcwIcon,

View File

@@ -6,6 +6,7 @@ import { useKey } from 'react-use';
import type { IconProps } from './Icon'; import type { IconProps } from './Icon';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { VStack } from './Stacks';
export interface ToastProps { export interface ToastProps {
children: ReactNode; children: ReactNode;
@@ -63,7 +64,7 @@ export function Toast({
'text-fg', 'text-fg',
)} )}
> >
<div className="px-3 py-2 flex items-center gap-2"> <div className="px-3 py-3 flex items-center gap-2">
{variant != null && ( {variant != null && (
<Icon <Icon
icon={ICONS[variant]} icon={ICONS[variant]}
@@ -75,10 +76,10 @@ export function Toast({
)} )}
/> />
)} )}
<div className="flex flex-col gap-1 w-full"> <VStack space={2}>
<div>{children}</div> <div>{children}</div>
{action} {action}
</div> </VStack>
</div> </div>
<IconButton <IconButton

View File

@@ -49,7 +49,7 @@ export const routePaths = {
}; };
export function useAppRoutes() { export function useAppRoutes() {
const workspaceId = useActiveWorkspaceId(); const activeWorkspaceId = useActiveWorkspaceId();
const requestId = useActiveRequestId(); const requestId = useActiveRequestId();
const nav = useNavigate(); const nav = useNavigate();
@@ -66,22 +66,22 @@ export function useAppRoutes() {
const setEnvironment = useCallback( const setEnvironment = useCallback(
(environment: Environment | null) => { (environment: Environment | null) => {
if (workspaceId == null) { if (activeWorkspaceId == null) {
navigate('workspaces'); navigate('workspaces');
} else if (requestId == null) { } else if (requestId == null) {
navigate('workspace', { navigate('workspace', {
workspaceId: workspaceId, workspaceId: activeWorkspaceId,
environmentId: environment == null ? undefined : environment.id, environmentId: environment == null ? undefined : environment.id,
}); });
} else { } else {
navigate('request', { navigate('request', {
workspaceId, workspaceId: activeWorkspaceId,
environmentId: environment == null ? undefined : environment.id, environmentId: environment == null ? undefined : environment.id,
requestId, requestId,
}); });
} }
}, },
[navigate, workspaceId, requestId], [navigate, activeWorkspaceId, requestId],
); );
return { return {

View File

@@ -57,7 +57,6 @@ export function useGrpc(
)) as ReflectResponseService[], )) as ReflectResponseService[],
}); });
console.log('CONN', conn);
return { return {
go, go,
reflect, reflect,

View File

@@ -0,0 +1,33 @@
import { useMutation } from '@tanstack/react-query';
import React from 'react';
import { useDialog } from '../components/DialogContext';
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useRequests } from './useRequests';
export function useMoveToWorkspace(id: string) {
const dialog = useDialog();
const requests = useRequests();
const request = requests.find((r) => r.id === id);
const activeWorkspaceId = useActiveWorkspaceId();
return useMutation<void, unknown>({
mutationKey: ['moveWorkspace'],
mutationFn: async () => {
if (request == null || activeWorkspaceId == null) return;
dialog.show({
id: 'change-workspace',
title: 'Change Workspace',
size: 'sm',
render: ({ hide }) => (
<MoveToWorkspaceDialog
onDone={hide}
request={request}
activeWorkspaceId={activeWorkspaceId}
/>
),
});
},
});
}