mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-11 21:11:36 +01:00
Good start to multi-window
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { persistQueryClient } from '@tanstack/react-query-persist-client';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
@@ -21,6 +20,7 @@ import { extractKeyValue, getKeyValue, setKeyValue } from '../lib/keyValueStore'
|
||||
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
|
||||
import { AppRouter } from './AppRouter';
|
||||
import { DialogProvider } from './DialogContext';
|
||||
import { appWindow, WebviewWindow } from '@tauri-apps/api/window';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -43,10 +43,13 @@ persistQueryClient({
|
||||
});
|
||||
|
||||
await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => {
|
||||
if (keyValue.updatedBy === appWindow.label) return;
|
||||
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue));
|
||||
});
|
||||
|
||||
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {
|
||||
if (request.updatedBy === appWindow.label) return;
|
||||
|
||||
queryClient.setQueryData(
|
||||
requestsQueryKey(request.workspaceId),
|
||||
(requests: HttpRequest[] = []) => {
|
||||
@@ -72,6 +75,8 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
|
||||
queryClient.setQueryData(
|
||||
responsesQueryKey(response.requestId),
|
||||
(responses: HttpResponse[] = []) => {
|
||||
if (response.updatedBy === appWindow.label) return;
|
||||
|
||||
const newResponses = [];
|
||||
let found = false;
|
||||
for (const r of responses) {
|
||||
@@ -92,6 +97,8 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
|
||||
|
||||
await listen('updated_workspace', ({ payload: workspace }: { payload: Workspace }) => {
|
||||
queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => {
|
||||
if (workspace.updatedBy === appWindow.label) return;
|
||||
|
||||
const newWorkspaces = [];
|
||||
let found = false;
|
||||
for (const w of workspaces) {
|
||||
@@ -175,7 +182,7 @@ export function App() {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<DialogProvider>
|
||||
<AppRouter />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{/*<ReactQueryDevtools initialIsOpen={false} />*/}
|
||||
</DialogProvider>
|
||||
</DndProvider>
|
||||
</HelmetProvider>
|
||||
|
||||
@@ -62,9 +62,14 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
});
|
||||
const req: HttpRequest = { ...baseRequest, body, id: '' };
|
||||
sendEphemeralRequest(req).then((response) => {
|
||||
const { data } = JSON.parse(response.body);
|
||||
const schema = buildClientSchema(data);
|
||||
setGraphqlExtension(graphql(schema, {}));
|
||||
try {
|
||||
const { data } = JSON.parse(response.body);
|
||||
const schema = buildClientSchema(data);
|
||||
setGraphqlExtension(graphql(schema, {}));
|
||||
} catch (err) {
|
||||
console.log('Failed to parse introspection query', err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [baseRequest.url]);
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Pr
|
||||
onClick={onClose}
|
||||
className="absolute inset-0 bg-gray-600/60 dark:bg-black/50"
|
||||
/>
|
||||
{/* Add region to still be able to drag the window */}
|
||||
<div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" />
|
||||
{children}
|
||||
</motion.div>
|
||||
</FocusTrap>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import classnames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
@@ -6,7 +7,7 @@ import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpHeader, HttpRequest } from '../lib/models';
|
||||
import { HttpRequestBodyType } from '../lib/models';
|
||||
import { BODY_TYPE_GRAPHQL, BODY_TYPE_JSON, BODY_TYPE_NONE, BODY_TYPE_XML } from '../lib/models';
|
||||
import { Editor } from './core/Editor';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
@@ -32,45 +33,64 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
defaultValue: 'body',
|
||||
});
|
||||
|
||||
const tabs: TabItem<HttpRequest['bodyType']>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: 'body',
|
||||
label: activeRequest?.bodyType ?? 'No Body',
|
||||
options: {
|
||||
onChange: async (bodyType: HttpRequest['bodyType']) => {
|
||||
const patch: Partial<HttpRequest> = { bodyType };
|
||||
if (bodyType == HttpRequestBodyType.GraphQL) {
|
||||
patch.method = 'POST';
|
||||
patch.headers = [
|
||||
...(activeRequest?.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
|
||||
[]),
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
enabled: true,
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() =>
|
||||
activeRequest === null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
value: 'body',
|
||||
options: {
|
||||
value: activeRequest.bodyType,
|
||||
items: [
|
||||
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
|
||||
{ label: 'JSON', value: BODY_TYPE_JSON },
|
||||
{ label: 'XML', value: BODY_TYPE_XML },
|
||||
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
||||
],
|
||||
onChange: async (bodyType) => {
|
||||
const patch: Partial<HttpRequest> = { bodyType };
|
||||
if (bodyType == BODY_TYPE_GRAPHQL) {
|
||||
patch.method = 'POST';
|
||||
patch.headers = [
|
||||
...(activeRequest?.headers.filter(
|
||||
(h) => h.name.toLowerCase() !== 'content-type',
|
||||
) ?? []),
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
setTimeout(() => {
|
||||
setForceUpdateHeaderEditorKey((u) => u + 1);
|
||||
}, 100);
|
||||
}
|
||||
await updateRequest.mutate(patch);
|
||||
},
|
||||
];
|
||||
setTimeout(() => {
|
||||
setForceUpdateHeaderEditorKey((u) => u + 1);
|
||||
}, 100);
|
||||
}
|
||||
await updateRequest.mutate(patch);
|
||||
},
|
||||
value: activeRequest?.bodyType ?? null,
|
||||
items: [
|
||||
{ label: 'No Body', value: null },
|
||||
{ label: 'JSON', value: HttpRequestBodyType.JSON },
|
||||
{ label: 'XML', value: HttpRequestBodyType.XML },
|
||||
{ label: 'GraphQL', value: HttpRequestBodyType.GraphQL },
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'auth',
|
||||
label: 'Auth',
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
{ label: 'No Auth', shortLabel: 'Auth', value: null },
|
||||
{ label: 'Basic', value: 'basic' },
|
||||
],
|
||||
onChange: async (a) => {
|
||||
await updateRequest.mutate({
|
||||
authenticationType: a,
|
||||
authentication: { username: '', password: '' },
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{ value: 'params', label: 'URL Params' },
|
||||
{ value: 'headers', label: 'Headers' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ value: 'params', label: 'URL Params' },
|
||||
{ value: 'headers', label: 'Headers' },
|
||||
{ value: 'auth', label: 'Auth' },
|
||||
],
|
||||
[activeRequest?.bodyType, activeRequest?.headers],
|
||||
[activeRequest?.bodyType, activeRequest?.headers, activeRequest?.authenticationType],
|
||||
);
|
||||
|
||||
const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []);
|
||||
@@ -79,6 +99,9 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
[],
|
||||
);
|
||||
|
||||
const forceUpdateKey =
|
||||
activeRequest?.updatedBy === appWindow.label ? undefined : activeRequest?.updatedAt;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
@@ -86,7 +109,12 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
>
|
||||
{activeRequest && (
|
||||
<>
|
||||
<UrlBar id={activeRequest.id} url={activeRequest.url} method={activeRequest.method} />
|
||||
<UrlBar
|
||||
key={forceUpdateKey}
|
||||
id={activeRequest.id}
|
||||
url={activeRequest.url}
|
||||
method={activeRequest.method}
|
||||
/>
|
||||
<Tabs
|
||||
value={activeTab.value}
|
||||
onChangeValue={activeTab.set}
|
||||
@@ -110,7 +138,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} />
|
||||
</TabContent>
|
||||
<TabContent value="body" className="pl-3 mt-1">
|
||||
{activeRequest.bodyType === HttpRequestBodyType.JSON ? (
|
||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||
<Editor
|
||||
key={activeRequest.id}
|
||||
useTemplating
|
||||
@@ -122,7 +150,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
onChange={handleBodyChange}
|
||||
format={(v) => tryFormatJson(v)}
|
||||
/>
|
||||
) : activeRequest.bodyType === HttpRequestBodyType.XML ? (
|
||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||
<Editor
|
||||
key={activeRequest.id}
|
||||
useTemplating
|
||||
@@ -133,7 +161,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
contentType="text/xml"
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === HttpRequestBodyType.GraphQL ? (
|
||||
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
||||
<GraphQLEditor
|
||||
key={activeRequest.id}
|
||||
baseRequest={activeRequest}
|
||||
|
||||
@@ -10,6 +10,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useSidebarDisplay } from '../hooks/useSidebarDisplay';
|
||||
import { WINDOW_FLOATING_SIDEBAR_WIDTH } from '../lib/constants';
|
||||
import { Button } from './core/Button';
|
||||
import { Overlay } from './Overlay';
|
||||
import { RequestResponse } from './RequestResponse';
|
||||
import { ResizeHandle } from './ResizeHandle';
|
||||
@@ -90,6 +91,14 @@ export default function Workspace() {
|
||||
[sideWidth, floating],
|
||||
);
|
||||
|
||||
if (windowSize.width <= 100) {
|
||||
return (
|
||||
<div>
|
||||
<Button>Send</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={styles}
|
||||
|
||||
@@ -45,7 +45,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
|
||||
className,
|
||||
'outline-none whitespace-nowrap',
|
||||
// 'border border-transparent focus-visible:border-focus',
|
||||
'focus-visible:ring ring-blue-300',
|
||||
'focus-visible-or-class:ring ring-blue-300',
|
||||
'rounded-md flex items-center',
|
||||
colorStyles[color || 'default'],
|
||||
justify === 'start' && 'justify-start',
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { useKeyPressEvent, useMount } from 'react-use';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { Portal } from '../Portal';
|
||||
import { Separator } from './Separator';
|
||||
import { VStack } from './Stacks';
|
||||
@@ -83,10 +83,6 @@ interface MenuProps {
|
||||
function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
||||
if (triggerRect === undefined) return null;
|
||||
|
||||
useMount(() => {
|
||||
console.log(document.activeElement);
|
||||
});
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
|
||||
|
||||
@@ -98,6 +94,10 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
||||
setMenuStyles({ maxHeight: windowBox.height - menuBox.top - 5 });
|
||||
}, []);
|
||||
|
||||
useKeyPressEvent('Escape', () => {
|
||||
onClose();
|
||||
});
|
||||
|
||||
useKeyPressEvent('ArrowUp', () => {
|
||||
setSelectedIndex((currIndex) => {
|
||||
let nextIndex = (currIndex ?? 0) - 1;
|
||||
|
||||
@@ -5,10 +5,11 @@ import { Icon } from './Icon';
|
||||
|
||||
export interface RadioDropdownItem<T> {
|
||||
label: string;
|
||||
shortLabel?: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export interface RadioDropdownProps<T> {
|
||||
export interface RadioDropdownProps<T = string | null> {
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
items: RadioDropdownItem<T>[];
|
||||
@@ -18,8 +19,9 @@ export interface RadioDropdownProps<T> {
|
||||
export function RadioDropdown<T>({ value, items, onChange, children }: RadioDropdownProps<T>) {
|
||||
const dropdownItems = useMemo(
|
||||
() =>
|
||||
items.map(({ label, value: v }) => ({
|
||||
items.map(({ label, shortLabel, value: v }) => ({
|
||||
label,
|
||||
shortLabel,
|
||||
onSelect: () => onChange(v),
|
||||
leftSlot: <Icon icon={value === v ? 'check' : 'empty'} />,
|
||||
})),
|
||||
|
||||
@@ -7,23 +7,27 @@ import type { RadioDropdownProps } from '../RadioDropdown';
|
||||
import { RadioDropdown } from '../RadioDropdown';
|
||||
import { HStack } from '../Stacks';
|
||||
|
||||
export type TabItem<T = string> = {
|
||||
value: string;
|
||||
label: string;
|
||||
options?: Omit<RadioDropdownProps<T>, 'children'>;
|
||||
};
|
||||
export type TabItem =
|
||||
| {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
| {
|
||||
value: string;
|
||||
options: Omit<RadioDropdownProps, 'children'>;
|
||||
};
|
||||
|
||||
interface Props<T = unknown> {
|
||||
interface Props {
|
||||
label: string;
|
||||
value?: string;
|
||||
onChangeValue: (value: string) => void;
|
||||
tabs: TabItem<T>[];
|
||||
tabs: TabItem[];
|
||||
tabListClassName?: string;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Tabs<T>({
|
||||
export function Tabs({
|
||||
value,
|
||||
onChangeValue,
|
||||
label,
|
||||
@@ -31,7 +35,7 @@ export function Tabs<T>({
|
||||
tabs,
|
||||
className,
|
||||
tabListClassName,
|
||||
}: Props<T>) {
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
@@ -77,7 +81,8 @@ export function Tabs<T>({
|
||||
const btnClassName = classnames(
|
||||
isActive ? 'bg-gray-100 text-gray-800' : 'text-gray-600 hover:text-gray-900',
|
||||
);
|
||||
if (t.options) {
|
||||
if ('options' in t) {
|
||||
const option = t.options.items.find((i) => i.value === t.options?.value);
|
||||
return (
|
||||
<RadioDropdown
|
||||
key={t.value}
|
||||
@@ -91,7 +96,7 @@ export function Tabs<T>({
|
||||
onClick={isActive ? undefined : () => handleTabChange(t.value)}
|
||||
className={btnClassName}
|
||||
>
|
||||
{t.options.items.find((i) => i.value === t.options?.value)?.label ?? ''}
|
||||
{option?.shortLabel ?? option?.label ?? 'Unknown'}
|
||||
<Icon icon="triangleDown" className="-mr-1.5" />
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
|
||||
@@ -13,10 +13,10 @@ export function Confirm({ hide }: Props) {
|
||||
|
||||
return (
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button color="gray" onClick={hide}>
|
||||
<Button className="focus" color="gray" onClick={hide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button ref={focusRef} color="primary">
|
||||
<Button className="focus" ref={focusRef} color="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { useMemo } from 'react';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useWorkspaces } from './useWorkspaces';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { getRequest } from '../lib/store';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
|
||||
export function useUpdateRequest(id: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, unknown, Partial<HttpRequest>>({
|
||||
mutationFn: async (patch) => {
|
||||
const request = await getRequest(id);
|
||||
@@ -13,9 +15,19 @@ export function useUpdateRequest(id: string | null) {
|
||||
|
||||
const updatedRequest = { ...request, ...patch };
|
||||
|
||||
console.log('UPDATING REQUEST', patch);
|
||||
await invoke('update_request', {
|
||||
request: updatedRequest,
|
||||
});
|
||||
},
|
||||
onMutate: async (patch) => {
|
||||
const request = await getRequest(id);
|
||||
if (request === null) return;
|
||||
queryClient.setQueryData(
|
||||
requestsQueryKey(request?.workspaceId),
|
||||
(requests: HttpRequest[] | undefined) =>
|
||||
requests?.map((r) => (r.id === request.id ? { ...r, ...patch } : r)),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export interface BaseModel {
|
||||
readonly id: string;
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt: Date;
|
||||
readonly createdAt: string;
|
||||
readonly updatedAt: string;
|
||||
readonly updatedBy: string;
|
||||
}
|
||||
|
||||
export interface Workspace extends BaseModel {
|
||||
@@ -16,11 +17,13 @@ export interface HttpHeader {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export enum HttpRequestBodyType {
|
||||
GraphQL = 'graphql',
|
||||
JSON = 'application/json',
|
||||
XML = 'text/xml',
|
||||
}
|
||||
export const BODY_TYPE_NONE = null;
|
||||
export const BODY_TYPE_GRAPHQL = 'graphql';
|
||||
export const BODY_TYPE_JSON = 'application/json';
|
||||
export const BODY_TYPE_XML = 'text/xml';
|
||||
|
||||
export const AUTH_TYPE_NONE = null;
|
||||
export const AUTH_TYPE_BASIC = 'basic';
|
||||
|
||||
export interface HttpRequest extends BaseModel {
|
||||
readonly workspaceId: string;
|
||||
@@ -29,7 +32,11 @@ export interface HttpRequest extends BaseModel {
|
||||
name: string;
|
||||
url: string;
|
||||
body: string | null;
|
||||
bodyType: HttpRequestBodyType | null;
|
||||
bodyType: string | null;
|
||||
authentication: any | null;
|
||||
authenticationType: string | null;
|
||||
auth: Record<string, string | number | null>;
|
||||
authType: string | null;
|
||||
method: string;
|
||||
headers: HttpHeader[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user