diff --git a/package-lock.json b/package-lock.json index 7b01d181..16bc70dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,10 @@ "@lezer/lr": "^1.3.3", "@radix-ui/react-icons": "^1.2.0", "@tailwindcss/container-queries": "^0.1.0", - "@tanstack/react-query": "^4.24.10", - "@tanstack/react-query-devtools": "^4.26.1", + "@tanstack/query-sync-storage-persister": "^4.27.1", + "@tanstack/react-query": "^4.28.0", + "@tanstack/react-query-devtools": "^4.28.0", + "@tanstack/react-query-persist-client": "^4.28.0", "@tauri-apps/api": "^1.2.0", "@vitejs/plugin-react": "^3.1.0", "classnames": "^2.3.2", @@ -1485,20 +1487,44 @@ } }, "node_modules/@tanstack/query-core": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.26.1.tgz", - "integrity": "sha512-Zrx2pVQUP4ndnsu6+K/m8zerXSVY8QM+YSbxA1/jbBY21GeCd5oKfYl92oXPK0hPEUtoNuunIdiq0ZMqLos+Zg==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.27.0.tgz", + "integrity": "sha512-sm+QncWaPmM73IPwFlmWSKPqjdTXZeFf/7aEmWh00z7yl2FjqophPt0dE1EHW9P1giMC5rMviv7OUbSDmWzXXA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-persist-client-core": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.27.0.tgz", + "integrity": "sha512-A+dPA7zG0MJOMDeBc/2WcKXW4wV2JMkeBVydobPW9G02M4q0yAj7vI+7SmM2dFuXyIvxXp4KulCywN6abRKDSQ==", + "dependencies": { + "@tanstack/query-core": "4.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-sync-storage-persister": { + "version": "4.27.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.27.1.tgz", + "integrity": "sha512-vClLXtyQZwfV8QTyxqfkEzZSuwIKnrxORAUyxvCDna1M9xao0HtKYsChPVaJoSZ42PNGGvKCiKdg4kfyLeWj+A==", + "dependencies": { + "@tanstack/query-persist-client-core": "4.27.0" + }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.26.1.tgz", - "integrity": "sha512-i3dnz4TOARGIXrXQ5P7S25Zfi4noii/bxhcwPurh2nrf5EUCcAt/95TB2HSmMweUBx206yIMWUMEQ7ptd6zwDg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.28.0.tgz", + "integrity": "sha512-8cGBV5300RHlvYdS4ea+G1JcZIt5CIuprXYFnsWggkmGoC0b5JaqG0fIX3qwDL9PTNkKvG76NGThIWbpXivMrQ==", "dependencies": { - "@tanstack/query-core": "4.26.1", + "@tanstack/query-core": "4.27.0", "use-sync-external-store": "^1.2.0" }, "funding": { @@ -1520,9 +1546,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.26.1.tgz", - "integrity": "sha512-ts2mA+fyFYFRi3Cee4xBk8Fx6waSFOM+yCkFqwJfGQRGjjTIMYMZPJv4wkv7vy12IVi1SYhL8au22LRKlXS1Zg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.28.0.tgz", + "integrity": "sha512-1SnoMw1CWn8FdPEIHvlAzmMBX3heXJo11fyBtt+FzYAHj5yFC8P67Kpgi0HpLkY7SLnd6QK/7qFkpeH4AQbgZg==", "dependencies": { "@tanstack/match-sorter-utils": "^8.7.0", "superjson": "^1.10.0", @@ -1533,11 +1559,26 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "4.26.1", + "@tanstack/react-query": "4.28.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@tanstack/react-query-persist-client": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.28.0.tgz", + "integrity": "sha512-xNpi3YdPOQIyYkKhByYDqTlyCeqICWFhV5PWkoVxYfzlRK6HYX4s+9Int407jEvhBz9cGC4OaL7rd6bynCFrYg==", + "dependencies": { + "@tanstack/query-persist-client-core": "4.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "4.28.0" + } + }, "node_modules/@tauri-apps/api": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.2.0.tgz", @@ -8257,29 +8298,53 @@ } }, "@tanstack/query-core": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.26.1.tgz", - "integrity": "sha512-Zrx2pVQUP4ndnsu6+K/m8zerXSVY8QM+YSbxA1/jbBY21GeCd5oKfYl92oXPK0hPEUtoNuunIdiq0ZMqLos+Zg==" + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.27.0.tgz", + "integrity": "sha512-sm+QncWaPmM73IPwFlmWSKPqjdTXZeFf/7aEmWh00z7yl2FjqophPt0dE1EHW9P1giMC5rMviv7OUbSDmWzXXA==" + }, + "@tanstack/query-persist-client-core": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.27.0.tgz", + "integrity": "sha512-A+dPA7zG0MJOMDeBc/2WcKXW4wV2JMkeBVydobPW9G02M4q0yAj7vI+7SmM2dFuXyIvxXp4KulCywN6abRKDSQ==", + "requires": { + "@tanstack/query-core": "4.27.0" + } + }, + "@tanstack/query-sync-storage-persister": { + "version": "4.27.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.27.1.tgz", + "integrity": "sha512-vClLXtyQZwfV8QTyxqfkEzZSuwIKnrxORAUyxvCDna1M9xao0HtKYsChPVaJoSZ42PNGGvKCiKdg4kfyLeWj+A==", + "requires": { + "@tanstack/query-persist-client-core": "4.27.0" + } }, "@tanstack/react-query": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.26.1.tgz", - "integrity": "sha512-i3dnz4TOARGIXrXQ5P7S25Zfi4noii/bxhcwPurh2nrf5EUCcAt/95TB2HSmMweUBx206yIMWUMEQ7ptd6zwDg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.28.0.tgz", + "integrity": "sha512-8cGBV5300RHlvYdS4ea+G1JcZIt5CIuprXYFnsWggkmGoC0b5JaqG0fIX3qwDL9PTNkKvG76NGThIWbpXivMrQ==", "requires": { - "@tanstack/query-core": "4.26.1", + "@tanstack/query-core": "4.27.0", "use-sync-external-store": "^1.2.0" } }, "@tanstack/react-query-devtools": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.26.1.tgz", - "integrity": "sha512-ts2mA+fyFYFRi3Cee4xBk8Fx6waSFOM+yCkFqwJfGQRGjjTIMYMZPJv4wkv7vy12IVi1SYhL8au22LRKlXS1Zg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.28.0.tgz", + "integrity": "sha512-1SnoMw1CWn8FdPEIHvlAzmMBX3heXJo11fyBtt+FzYAHj5yFC8P67Kpgi0HpLkY7SLnd6QK/7qFkpeH4AQbgZg==", "requires": { "@tanstack/match-sorter-utils": "^8.7.0", "superjson": "^1.10.0", "use-sync-external-store": "^1.2.0" } }, + "@tanstack/react-query-persist-client": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.28.0.tgz", + "integrity": "sha512-xNpi3YdPOQIyYkKhByYDqTlyCeqICWFhV5PWkoVxYfzlRK6HYX4s+9Int407jEvhBz9cGC4OaL7rd6bynCFrYg==", + "requires": { + "@tanstack/query-persist-client-core": "4.27.0" + } + }, "@tauri-apps/api": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.2.0.tgz", diff --git a/package.json b/package.json index 062b6ddf..6ac2ed89 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,10 @@ "@lezer/lr": "^1.3.3", "@radix-ui/react-icons": "^1.2.0", "@tailwindcss/container-queries": "^0.1.0", - "@tanstack/react-query": "^4.24.10", - "@tanstack/react-query-devtools": "^4.26.1", + "@tanstack/query-sync-storage-persister": "^4.27.1", + "@tanstack/react-query": "^4.28.0", + "@tanstack/react-query-devtools": "^4.28.0", + "@tanstack/react-query-persist-client": "^4.28.0", "@tauri-apps/api": "^1.2.0", "@vitejs/plugin-react": "^3.1.0", "classnames": "^2.3.2", diff --git a/src-web/components/App.tsx b/src-web/components/App.tsx index f1e7ec4b..88cf339a 100644 --- a/src-web/components/App.tsx +++ b/src-web/components/App.tsx @@ -1,5 +1,7 @@ +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'; import { MotionConfig } from 'framer-motion'; @@ -18,7 +20,25 @@ import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/mode import { convertDates } from '../lib/models'; import { AppRouter } from './AppRouter'; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + cacheTime: 1000 * 60 * 60 * 24, // 24 hours + networkMode: 'offlineFirst', + }, + }, +}); + +const localStoragePersister = createSyncStoragePersister({ + storage: window.localStorage, + throttleTime: 1000, +}); + +persistQueryClient({ + queryClient, + persister: localStoragePersister, + maxAge: 1000 * 60 * 60 * 24, // 24 hours +}); await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => { queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue)); diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 480c4926..415d20de 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -4,7 +4,8 @@ import { useActiveRequest } from '../hooks/useActiveRequest'; import { useKeyValue } from '../hooks/useKeyValue'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { tryFormatJson } from '../lib/formatters'; -import type { HttpHeader } from '../lib/models'; +import type { HttpHeader, HttpRequest } from '../lib/models'; +import { HttpRequestBodyType } from '../lib/models'; import { Editor } from './core/Editor'; import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; @@ -27,19 +28,25 @@ export function RequestPane({ fullHeight, className }: Props) { defaultValue: 'body', }); - const tabs: TabItem[] = useMemo( + const tabs: TabItem[] = useMemo( () => [ { value: 'body', label: activeRequest?.bodyType ?? 'No Body', options: { - onChange: (bodyType: string | null) => updateRequest.mutate({ bodyType }), + onChange: (bodyType: HttpRequest['bodyType']) => { + const patch: Partial = { bodyType }; + if (bodyType == HttpRequestBodyType.GraphQL) { + patch.method = 'POST'; + } + updateRequest.mutate(patch); + }, value: activeRequest?.bodyType ?? null, items: [ { label: 'No Body', value: null }, - { label: 'JSON', value: 'json' }, - { label: 'XML', value: 'xml' }, - { label: 'GraphQL', value: 'graphql' }, + { label: 'JSON', value: HttpRequestBodyType.JSON }, + { label: 'XML', value: HttpRequestBodyType.XML }, + { label: 'GraphQL', value: HttpRequestBodyType.GraphQL }, ], }, }, @@ -85,7 +92,7 @@ export function RequestPane({ fullHeight, className }: Props) { null} /> - {activeRequest.bodyType === 'json' ? ( + {activeRequest.bodyType === HttpRequestBodyType.JSON ? ( tryFormatJson(v)} /> - ) : activeRequest.bodyType === 'xml' ? ( + ) : activeRequest.bodyType === HttpRequestBodyType.XML ? ( - ) : activeRequest.bodyType === 'graphql' ? ( + ) : activeRequest.bodyType === HttpRequestBodyType.GraphQL ? ( 700; + const isSideBySide = mainContentWidth > 900; if (activeWorkspace == null) { return null; diff --git a/src-web/components/WorkspaceDropdown.tsx b/src-web/components/WorkspaceDropdown.tsx index 342c9c41..cb6a90f7 100644 --- a/src-web/components/WorkspaceDropdown.tsx +++ b/src-web/components/WorkspaceDropdown.tsx @@ -1,5 +1,6 @@ import classnames from 'classnames'; import { memo, useMemo } from 'react'; +import { act } from 'react-dom/test-utils'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId'; import { useCreateWorkspace } from '../hooks/useCreateWorkspace'; @@ -16,12 +17,12 @@ type Props = { }; export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }: Props) { - const routes = useRoutes(); const workspaces = useWorkspaces(); const activeWorkspace = useActiveWorkspace(); - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspaceId = activeWorkspace?.id ?? null; const createWorkspace = useCreateWorkspace({ navigateAfter: true }); const deleteWorkspace = useDeleteWorkspace(activeWorkspaceId); + const routes = useRoutes(); const items: DropdownItem[] = useMemo(() => { const workspaceItems = workspaces.map((w) => ({ @@ -52,7 +53,7 @@ export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }: return ( ); diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index d8a608b0..60d2f508 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -271,6 +271,7 @@ const FormRow = memo(function FormRow({ onChange={handleChangeEnabled} />
e.preventDefault()} className={classnames( 'grid items-center', '@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]', diff --git a/src-web/components/core/Tabs/Tabs.css b/src-web/components/core/Tabs/Tabs.css deleted file mode 100644 index ba7841d1..00000000 --- a/src-web/components/core/Tabs/Tabs.css +++ /dev/null @@ -1,5 +0,0 @@ -.tab-content { - &[data-state="inactive"] { - @apply hidden; - } -} diff --git a/src-web/components/core/Tabs/Tabs.tsx b/src-web/components/core/Tabs/Tabs.tsx index 0410af38..96376f96 100644 --- a/src-web/components/core/Tabs/Tabs.tsx +++ b/src-web/components/core/Tabs/Tabs.tsx @@ -7,9 +7,7 @@ import type { RadioDropdownProps } from '../RadioDropdown'; import { RadioDropdown } from '../RadioDropdown'; import { HStack } from '../Stacks'; -import './Tabs.css'; - -export type TabItem = { +export type TabItem = { value: string; label: string; options?: Omit, 'children'>; @@ -37,14 +35,18 @@ export function Tabs({ const ref = useRef(null); const handleTabChange = (value: string) => { - const tabs = ref.current?.querySelectorAll(`[data-tab]`); + const tabs = ref.current?.querySelectorAll(`[data-tab]`); for (const tab of tabs ?? []) { const v = tab.getAttribute('data-tab'); if (v === value) { tab.setAttribute('tabindex', '-1'); tab.setAttribute('data-state', 'active'); + tab.setAttribute('aria-hidden', 'false'); + tab.style.display = 'block'; } else { tab.setAttribute('data-state', 'inactive'); + tab.setAttribute('aria-hidden', 'true'); + tab.style.display = 'none'; } } onChangeValue(value); diff --git a/src-web/hooks/useActiveWorkspace.ts b/src-web/hooks/useActiveWorkspace.ts index 479ed710..f633637c 100644 --- a/src-web/hooks/useActiveWorkspace.ts +++ b/src-web/hooks/useActiveWorkspace.ts @@ -1,16 +1,14 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { act } from 'react-dom/test-utils'; import type { Workspace } from '../lib/models'; import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useWorkspaces } from './useWorkspaces'; export function useActiveWorkspace(): Workspace | null { - const workspaces = useWorkspaces(); const workspaceId = useActiveWorkspaceId(); - const [activeWorkspace, setActiveWorkspace] = useState(null); - - useEffect(() => { - setActiveWorkspace(workspaces.find((w) => w.id === workspaceId) ?? null); - }, [workspaces, workspaceId]); - - return activeWorkspace; + const workspaces = useWorkspaces(); + return useMemo( + () => workspaces.find((w) => w.id === workspaceId) ?? null, + [workspaces, workspaceId], + ); } diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 756d9a12..e75505ce 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -16,6 +16,12 @@ export interface HttpHeader { enabled?: boolean; } +export enum HttpRequestBodyType { + GraphQL = 'graphql', + JSON = 'application/json', + XML = 'text/xml', +} + export interface HttpRequest extends BaseModel { readonly workspaceId: string; readonly model: 'http_request'; @@ -23,7 +29,7 @@ export interface HttpRequest extends BaseModel { name: string; url: string; body: string | null; - bodyType: string | null; + bodyType: HttpRequestBodyType | null; method: string; headers: HttpHeader[]; }