mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 00:58:32 +02:00
Toast component and use for copy-as-curl
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
"os:allow-os-type",
|
"os:allow-os-type",
|
||||||
"event:allow-emit",
|
"event:allow-emit",
|
||||||
"clipboard-manager:allow-write-text",
|
"clipboard-manager:allow-write-text",
|
||||||
|
"clipboard-manager:allow-read-text",
|
||||||
"dialog:allow-open",
|
"dialog:allow-open",
|
||||||
"dialog:allow-save",
|
"dialog:allow-save",
|
||||||
"event:allow-listen",
|
"event:allow-listen",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","clipboard-manager:default"]}}
|
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","clipboard-manager:default"]}}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { DialogProvider } from './DialogContext';
|
import { DialogProvider } from './DialogContext';
|
||||||
import { GlobalHooks } from './GlobalHooks';
|
import { GlobalHooks } from './GlobalHooks';
|
||||||
|
import { ToastProvider } from './ToastContext';
|
||||||
|
|
||||||
export function DefaultLayout() {
|
export function DefaultLayout() {
|
||||||
return (
|
return (
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<Outlet />
|
<ToastProvider>
|
||||||
<GlobalHooks />
|
<Outlet />
|
||||||
|
<GlobalHooks />
|
||||||
|
</ToastProvider>
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export function ImportDataDialog({ importData }: Props) {
|
|||||||
<VStack space={1}>
|
<VStack space={1}>
|
||||||
<p>Supported Formats:</p>
|
<p>Supported Formats:</p>
|
||||||
<ul className="list-disc pl-5">
|
<ul className="list-disc pl-5">
|
||||||
<li>Postman Collection v2/v2.1</li>
|
<li>Postman Collection v2+</li>
|
||||||
<li>Insomnia</li>
|
<li>Insomnia v4+</li>
|
||||||
<li>Curl command(s)</li>
|
<li>Curl command(s)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -250,15 +250,13 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
if (!command.startsWith('curl ')) {
|
if (!command.startsWith('curl ')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
const confirmed = await confirm({
|
||||||
await confirm({
|
id: 'paste-curl',
|
||||||
id: 'paste-curl',
|
title: 'Import from Curl?',
|
||||||
title: 'Import from Curl?',
|
description: 'Do you want to overwrite the current request with the Curl command?',
|
||||||
description:
|
confirmText: 'Overwrite',
|
||||||
'Do you want to overwrite the current request with the Curl command?',
|
});
|
||||||
confirmText: 'Overwrite',
|
if (confirmed) {
|
||||||
})
|
|
||||||
) {
|
|
||||||
importCurl.mutate({ requestId: activeRequestId, command });
|
importCurl.mutate({ requestId: activeRequestId, command });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
|||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||||
import { useCopyAsCurl } from '../hooks/useCopyAsCurl';
|
|
||||||
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
||||||
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||||
@@ -41,6 +40,7 @@ import { InlineCode } from './core/InlineCode';
|
|||||||
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';
|
||||||
|
import { useCopyAsCurl } from '../hooks/useCopyAsCurl';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -608,7 +608,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
|||||||
const deleteRequest = useDeleteRequest(activeRequest ?? null);
|
const deleteRequest = useDeleteRequest(activeRequest ?? null);
|
||||||
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
|
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
|
||||||
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
|
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
|
||||||
const [isCopied, copyAsCurl] = useCopyAsCurl(itemId);
|
const [copyAsCurl] = useCopyAsCurl(itemId);
|
||||||
const sendRequest = useSendRequest(itemId);
|
const sendRequest = useSendRequest(itemId);
|
||||||
const sendManyRequests = useSendManyRequests();
|
const sendManyRequests = useSendManyRequests();
|
||||||
const latestHttpResponse = useLatestHttpResponse(itemId);
|
const latestHttpResponse = useLatestHttpResponse(itemId);
|
||||||
@@ -739,12 +739,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
|||||||
{
|
{
|
||||||
key: 'copyCurl',
|
key: 'copyCurl',
|
||||||
label: 'Copy as Curl',
|
label: 'Copy as Curl',
|
||||||
leftSlot: (
|
leftSlot: <Icon icon="copy" />,
|
||||||
<Icon
|
|
||||||
className={isCopied ? 'text-green-500' : undefined}
|
|
||||||
icon={isCopied ? 'check' : 'copy'}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onSelect: copyAsCurl,
|
onSelect: copyAsCurl,
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
|||||||
79
src-web/components/ToastContext.tsx
Normal file
79
src-web/components/ToastContext.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { ToastProps } from './core/Toast';
|
||||||
|
import { Toast } from './core/Toast';
|
||||||
|
import { generateId } from '../lib/generateId';
|
||||||
|
import { Portal } from './Portal';
|
||||||
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
|
type ToastEntry = {
|
||||||
|
render: ({ hide }: { hide: () => void }) => React.ReactNode;
|
||||||
|
timeout?: number;
|
||||||
|
} & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>;
|
||||||
|
|
||||||
|
type PrivateToastEntry = ToastEntry & {
|
||||||
|
id: string;
|
||||||
|
timeout: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: PrivateToastEntry[];
|
||||||
|
actions: Actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Actions {
|
||||||
|
show: (d: ToastEntry) => void;
|
||||||
|
hide: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const ToastContext = createContext<State>({} as State);
|
||||||
|
|
||||||
|
export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [toasts, setToasts] = useState<State['toasts']>([]);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
const actions = useMemo<Actions>(
|
||||||
|
() => ({
|
||||||
|
show({ timeout = 4000, ...props }: ToastEntry) {
|
||||||
|
const id = generateId();
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
this.hide(id);
|
||||||
|
}, timeout);
|
||||||
|
setToasts((a) => [...a.filter((d) => d.id !== id), { id, timeout, ...props }]);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
hide: (id: string) => {
|
||||||
|
setToasts((a) => a.filter((d) => d.id !== id));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const state: State = { toasts, actions };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
<Portal name="toasts">
|
||||||
|
<div className="absolute right-0 bottom-0">
|
||||||
|
<AnimatePresence>
|
||||||
|
{toasts.map((props: PrivateToastEntry) => (
|
||||||
|
<ToastInstance key={props.id} {...props} />
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ToastInstance({ id, render, timeout, ...props }: PrivateToastEntry) {
|
||||||
|
const { actions } = useContext(ToastContext);
|
||||||
|
const children = render({ hide: () => actions.hide(id) });
|
||||||
|
return (
|
||||||
|
<Toast open timeout={timeout} onClose={() => actions.hide(id)} {...props}>
|
||||||
|
{children}
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useToast = () => useContext(ToastContext).actions;
|
||||||
@@ -33,6 +33,8 @@ import { ResizeHandle } from './ResizeHandle';
|
|||||||
import { Sidebar } from './Sidebar';
|
import { Sidebar } from './Sidebar';
|
||||||
import { SidebarActions } from './SidebarActions';
|
import { SidebarActions } from './SidebarActions';
|
||||||
import { WorkspaceHeader } from './WorkspaceHeader';
|
import { WorkspaceHeader } from './WorkspaceHeader';
|
||||||
|
import { useClipboardText } from '../hooks/useClipboardText';
|
||||||
|
import { Portal } from './Portal';
|
||||||
|
|
||||||
const side = { gridArea: 'side' };
|
const side = { gridArea: 'side' };
|
||||||
const head = { gridArea: 'head' };
|
const head = { gridArea: 'head' };
|
||||||
@@ -54,6 +56,7 @@ export default function Workspace() {
|
|||||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const isCurlInClipboard = !!useClipboardText()?.startsWith('curl ');
|
||||||
|
|
||||||
const unsub = () => {
|
const unsub = () => {
|
||||||
if (moveState.current !== null) {
|
if (moveState.current !== null) {
|
||||||
@@ -124,83 +127,88 @@ export default function Workspace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
style={styles}
|
<Portal name="toast">
|
||||||
className={classNames(
|
{isCurlInClipboard && <div className="static right-0 left-0 w-32 h-10">Import</div>}
|
||||||
'grid w-full h-full',
|
</Portal>
|
||||||
// Animate sidebar width changes but only when not resizing
|
<div
|
||||||
// because it's too slow to animate on mouse move
|
style={styles}
|
||||||
!isResizing && 'transition-all',
|
className={classNames(
|
||||||
)}
|
'grid w-full h-full',
|
||||||
>
|
// Animate sidebar width changes but only when not resizing
|
||||||
{floating ? (
|
// because it's too slow to animate on mouse move
|
||||||
<Overlay
|
!isResizing && 'transition-all',
|
||||||
open={!floatingSidebarHidden}
|
)}
|
||||||
portalName="sidebar"
|
>
|
||||||
onClose={() => setFloatingSidebarHidden(true)}
|
{floating ? (
|
||||||
>
|
<Overlay
|
||||||
<motion.div
|
open={!floatingSidebarHidden}
|
||||||
initial={{ opacity: 0, x: -20 }}
|
portalName="sidebar"
|
||||||
animate={{ opacity: 1, x: 0 }}
|
onClose={() => setFloatingSidebarHidden(true)}
|
||||||
className={classNames(
|
|
||||||
'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]',
|
|
||||||
'grid grid-rows-[auto_1fr]',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<HeaderSize className="border-transparent">
|
<motion.div
|
||||||
<SidebarActions />
|
initial={{ opacity: 0, x: -20 }}
|
||||||
</HeaderSize>
|
animate={{ opacity: 1, x: 0 }}
|
||||||
<Sidebar />
|
className={classNames(
|
||||||
</motion.div>
|
'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]',
|
||||||
</Overlay>
|
'grid grid-rows-[auto_1fr]',
|
||||||
) : (
|
)}
|
||||||
<>
|
>
|
||||||
<div style={side} className={classNames('overflow-hidden bg-gray-100')}>
|
<HeaderSize className="border-transparent">
|
||||||
<Sidebar className="border-r border-highlight" />
|
<SidebarActions />
|
||||||
|
</HeaderSize>
|
||||||
|
<Sidebar />
|
||||||
|
</motion.div>
|
||||||
|
</Overlay>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={side} className={classNames('overflow-hidden bg-gray-100')}>
|
||||||
|
<Sidebar className="border-r border-highlight" />
|
||||||
|
</div>
|
||||||
|
<ResizeHandle
|
||||||
|
className="-translate-x-3"
|
||||||
|
justify="end"
|
||||||
|
side="right"
|
||||||
|
isResizing={isResizing}
|
||||||
|
onResizeStart={handleResizeStart}
|
||||||
|
onReset={resetWidth}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<HeaderSize data-tauri-drag-region style={head}>
|
||||||
|
<WorkspaceHeader className="pointer-events-none" />
|
||||||
|
</HeaderSize>
|
||||||
|
{activeWorkspace == null ? (
|
||||||
|
<div className="m-auto">
|
||||||
|
<Banner color="warning" className="max-w-[30rem]">
|
||||||
|
The active workspace{' '}
|
||||||
|
<InlineCode className="text-orange-800">{activeWorkspaceId}</InlineCode> was not
|
||||||
|
found. Select a workspace from the header menu or report this bug to <FeedbackLink />
|
||||||
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
<ResizeHandle
|
) : activeRequest == null ? (
|
||||||
className="-translate-x-3"
|
<HotKeyList
|
||||||
justify="end"
|
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
|
||||||
side="right"
|
bottomSlot={
|
||||||
isResizing={isResizing}
|
<HStack space={1} justifyContent="center" className="mt-3">
|
||||||
onResizeStart={handleResizeStart}
|
<Button variant="border" size="sm" onClick={() => importData.mutate()}>
|
||||||
onReset={resetWidth}
|
Import
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<HeaderSize data-tauri-drag-region style={head}>
|
|
||||||
<WorkspaceHeader className="pointer-events-none" />
|
|
||||||
</HeaderSize>
|
|
||||||
{activeWorkspace == null ? (
|
|
||||||
<div className="m-auto">
|
|
||||||
<Banner color="warning" className="max-w-[30rem]">
|
|
||||||
The active workspace{' '}
|
|
||||||
<InlineCode className="text-orange-800">{activeWorkspaceId}</InlineCode> was not found.
|
|
||||||
Select a workspace from the header menu or report this bug to <FeedbackLink />
|
|
||||||
</Banner>
|
|
||||||
</div>
|
|
||||||
) : activeRequest == null ? (
|
|
||||||
<HotKeyList
|
|
||||||
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
|
|
||||||
bottomSlot={
|
|
||||||
<HStack space={1} justifyContent="center" className="mt-3">
|
|
||||||
<Button variant="border" size="sm" onClick={() => importData.mutate()}>
|
|
||||||
Import
|
|
||||||
</Button>
|
|
||||||
<CreateDropdown hideFolder>
|
|
||||||
<Button variant="border" forDropdown size="sm">
|
|
||||||
New Request
|
|
||||||
</Button>
|
</Button>
|
||||||
</CreateDropdown>
|
<CreateDropdown hideFolder>
|
||||||
</HStack>
|
<Button variant="border" forDropdown size="sm">
|
||||||
}
|
New Request
|
||||||
/>
|
</Button>
|
||||||
) : activeRequest.model === 'grpc_request' ? (
|
</CreateDropdown>
|
||||||
<GrpcConnectionLayout style={body} />
|
</HStack>
|
||||||
) : (
|
}
|
||||||
<HttpRequestLayout activeRequest={activeRequest} style={body} />
|
/>
|
||||||
)}
|
) : activeRequest.model === 'grpc_request' ? (
|
||||||
</div>
|
<GrpcConnectionLayout style={body} />
|
||||||
|
) : (
|
||||||
|
<HttpRequestLayout activeRequest={activeRequest} style={body} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
size === 'sm' && 'h-sm px-2.5 text-sm',
|
size === 'sm' && 'h-sm px-2.5 text-sm',
|
||||||
size === 'xs' && 'h-xs px-2 text-sm',
|
size === 'xs' && 'h-xs px-2 text-sm',
|
||||||
// Solids
|
// Solids
|
||||||
variant === 'solid' &&
|
variant === 'solid' && color === 'custom' && 'ring-blue-400',
|
||||||
color === 'custom' &&
|
|
||||||
'ring-blue-400 enabled:hocus:bg-highlightSecondary',
|
|
||||||
variant === 'solid' &&
|
variant === 'solid' &&
|
||||||
color === 'default' &&
|
color === 'default' &&
|
||||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400',
|
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400',
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const icons = {
|
|||||||
arrowUpFromDot: lucide.ArrowUpFromDotIcon,
|
arrowUpFromDot: lucide.ArrowUpFromDotIcon,
|
||||||
box: lucide.BoxIcon,
|
box: lucide.BoxIcon,
|
||||||
cake: lucide.CakeIcon,
|
cake: lucide.CakeIcon,
|
||||||
minus: lucide.MinusIcon,
|
|
||||||
chat: lucide.MessageSquare,
|
chat: lucide.MessageSquare,
|
||||||
check: lucide.CheckIcon,
|
check: lucide.CheckIcon,
|
||||||
chevronDown: lucide.ChevronDownIcon,
|
chevronDown: lucide.ChevronDownIcon,
|
||||||
@@ -25,6 +24,7 @@ const icons = {
|
|||||||
code: lucide.CodeIcon,
|
code: lucide.CodeIcon,
|
||||||
cookie: lucide.CookieIcon,
|
cookie: lucide.CookieIcon,
|
||||||
copy: lucide.CopyIcon,
|
copy: lucide.CopyIcon,
|
||||||
|
copyCheck: lucide.CopyCheck,
|
||||||
download: lucide.DownloadIcon,
|
download: lucide.DownloadIcon,
|
||||||
externalLink: lucide.ExternalLinkIcon,
|
externalLink: lucide.ExternalLinkIcon,
|
||||||
eye: lucide.EyeIcon,
|
eye: lucide.EyeIcon,
|
||||||
@@ -39,6 +39,7 @@ const icons = {
|
|||||||
leftPanelHidden: lucide.PanelLeftOpenIcon,
|
leftPanelHidden: lucide.PanelLeftOpenIcon,
|
||||||
leftPanelVisible: lucide.PanelLeftCloseIcon,
|
leftPanelVisible: lucide.PanelLeftCloseIcon,
|
||||||
magicWand: lucide.Wand2Icon,
|
magicWand: lucide.Wand2Icon,
|
||||||
|
minus: lucide.MinusIcon,
|
||||||
moreVertical: lucide.MoreVerticalIcon,
|
moreVertical: lucide.MoreVerticalIcon,
|
||||||
pencil: lucide.PencilIcon,
|
pencil: lucide.PencilIcon,
|
||||||
plug: lucide.Plug,
|
plug: lucide.Plug,
|
||||||
|
|||||||
76
src-web/components/core/Toast.tsx
Normal file
76
src-web/components/core/Toast.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useKey } from 'react-use';
|
||||||
|
import { Heading } from './Heading';
|
||||||
|
import { IconButton } from './IconButton';
|
||||||
|
|
||||||
|
export interface ToastProps {
|
||||||
|
children: ReactNode;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
timeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toast({ children, className, open, onClose, title, timeout }: ToastProps) {
|
||||||
|
const titleId = useMemo(() => Math.random().toString(36).slice(2), []);
|
||||||
|
|
||||||
|
useKey(
|
||||||
|
'Escape',
|
||||||
|
() => {
|
||||||
|
if (!open) return;
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
[open],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, right: '-10%' }}
|
||||||
|
animate={{ opacity: 100, right: 0 }}
|
||||||
|
exit={{ opacity: 0, right: '-100%' }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
'pointer-events-auto',
|
||||||
|
'relative bg-gray-50 dark:bg-gray-100 pointer-events-auto',
|
||||||
|
'rounded-lg',
|
||||||
|
'border border-highlightSecondary dark:border-highlight shadow-xl',
|
||||||
|
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
|
||||||
|
'w-[22rem] max-h-[80vh]',
|
||||||
|
'm-2 grid grid-cols-[1fr_auto]',
|
||||||
|
'text-gray-700',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="px-3 py-2">
|
||||||
|
{title && (
|
||||||
|
<Heading size={3} id={titleId}>
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">{children}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
color="custom"
|
||||||
|
className="opacity-50"
|
||||||
|
title="Dismiss"
|
||||||
|
icon="x"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<div className="w-full absolute bottom-0 left-0 right-0">
|
||||||
|
<motion.div
|
||||||
|
className="bg-highlight h-0.5"
|
||||||
|
initial={{ width: '100%' }}
|
||||||
|
animate={{ width: '0%', opacity: 0.2 }}
|
||||||
|
transition={{ duration: timeout / 1000, ease: 'linear' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src-web/hooks/useClipboardText.ts
Normal file
13
src-web/hooks/useClipboardText.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
|
|
||||||
|
export function useClipboardText() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [],
|
||||||
|
queryFn: async () => {
|
||||||
|
const text = await readText();
|
||||||
|
console.log('READ CLIPBOARD', text);
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
}).data;
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useToast } from '../components/ToastContext';
|
||||||
|
import { Icon } from '../components/core/Icon';
|
||||||
|
|
||||||
export function useCopyAsCurl(requestId: string) {
|
export function useCopyAsCurl(requestId: string) {
|
||||||
const [checked, setChecked] = useState<boolean>(false);
|
const [checked, setChecked] = useState<boolean>(false);
|
||||||
|
const toast = useToast();
|
||||||
return [
|
return [
|
||||||
checked,
|
checked,
|
||||||
async () => {
|
async () => {
|
||||||
@@ -11,6 +14,14 @@ export function useCopyAsCurl(requestId: string) {
|
|||||||
await writeText(cmd);
|
await writeText(cmd);
|
||||||
setChecked(true);
|
setChecked(true);
|
||||||
setTimeout(() => setChecked(false), 800);
|
setTimeout(() => setChecked(false), 800);
|
||||||
|
toast.show({
|
||||||
|
render: () => [
|
||||||
|
<>
|
||||||
|
<Icon icon="copyCheck" />
|
||||||
|
<span>Command copied to clipboard</span>
|
||||||
|
</>,
|
||||||
|
],
|
||||||
|
});
|
||||||
return cmd;
|
return cmd;
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
@@ -14,6 +14,7 @@ export type TrackResource =
|
|||||||
| 'key_value'
|
| 'key_value'
|
||||||
| 'setting'
|
| 'setting'
|
||||||
| 'sidebar'
|
| 'sidebar'
|
||||||
|
| 'toast'
|
||||||
| 'workspace';
|
| 'workspace';
|
||||||
|
|
||||||
export type TrackAction =
|
export type TrackAction =
|
||||||
|
|||||||
Reference in New Issue
Block a user