diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 7e6b3de3..96920039 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -75,7 +75,6 @@ export function RequestPane({ fullHeight, className }: Props) { ) : activeRequest.bodyType === 'graphql' ? ( updateRequest.mutate({ body })} diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 6ad81761..99c06b70 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -93,8 +93,6 @@ export function _Editor({ onFocus: handleFocus, readOnly, singleLine, - contentType, - useTemplating, }), ], }); @@ -132,15 +130,11 @@ function getExtensions({ singleLine, onChange, onFocus, - contentType, - useTemplating, -}: Pick<_EditorProps, 'singleLine' | 'contentType' | 'useTemplating' | 'readOnly'> & { +}: Pick<_EditorProps, 'singleLine' | 'readOnly'> & { container: HTMLDivElement | null; onChange: MutableRefObject<_EditorProps['onChange']>; onFocus: MutableRefObject<_EditorProps['onFocus']>; }) { - const ext = getLanguageExtension({ contentType, useTemplating }); - // TODO: Ensure tooltips render inside the dialog if we are in one. const parent = container?.closest('[role="dialog"]') ?? @@ -153,7 +147,6 @@ function getExtensions({ keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap), ...(singleLine ? [singleLineExt()] : []), ...(!singleLine ? [multiLineExtensions] : []), - ...(ext ? [ext] : []), ...(readOnly ? [EditorState.readOnly.of(true)] : []), ...(singleLine ? [ diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index 766d0d4d..d3a239ca 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -42,13 +42,17 @@ export const myHighlightStyle = HighlightStyle.define([ color: '#757b93', fontStyle: 'italic', }, + { + tag: [t.paren], + color: 'hsl(var(--color-gray-900))', + }, { tag: [t.name, t.tagName, t.angleBracket, t.docString, t.number], color: 'hsl(var(--color-blue-600))', }, { tag: [t.variableName], color: 'hsl(var(--color-green-600))' }, { tag: [t.bool], color: 'hsl(var(--color-pink-600))' }, - { tag: [t.attributeName], color: 'hsl(var(--color-violet-600))' }, + { tag: [t.attributeName, t.propertyName], color: 'hsl(var(--color-violet-600))' }, { tag: [t.attributeValue], color: 'hsl(var(--color-orange-600))' }, { tag: [t.string], color: 'hsl(var(--color-yellow-600))' }, { tag: [t.keyword, t.meta, t.operator], color: 'hsl(var(--color-red-600))' }, @@ -88,7 +92,7 @@ const syntaxExtensions: Record = { export function getLanguageExtension({ contentType, - useTemplating, + useTemplating = false, }: { contentType?: string; useTemplating?: boolean; diff --git a/src-web/components/core/IconButton.tsx b/src-web/components/core/IconButton.tsx index e2654cbd..f5eb62c6 100644 --- a/src-web/components/core/IconButton.tsx +++ b/src-web/components/core/IconButton.tsx @@ -20,7 +20,7 @@ export const IconButton = forwardRef(function IconButt 'text-gray-700 hover:text-gray-1000', '!px-0', size === 'md' && 'w-9', - size === 'sm' && 'w-9', + size === 'sm' && 'w-8', )} size={size} {...props} diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index 03b5f3ea..b7005a92 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -105,7 +105,7 @@ function FormRow({ onChange={(name) => onChange({ id, pair: { name, value: pairContainer.pair.value } })} onFocus={onFocus} placeholder={isLast ? 'new name' : 'name'} - useEditor={{ useTemplating: true }} + useEditor={{ useTemplating: true, contentType: 'text/plain' }} /> onChange({ id, pair: { name: pairContainer.pair.name, value } })} onFocus={onFocus} placeholder={isLast ? 'new value' : 'value'} - useEditor={{ useTemplating: true }} + useEditor={{ useTemplating: true, contentType: 'text/plain' }} /> {onDelete && ( {tabs.map((t) => { diff --git a/src-web/components/editors/GraphQLEditor.tsx b/src-web/components/editors/GraphQLEditor.tsx index 49c5a413..15a5533d 100644 --- a/src-web/components/editors/GraphQLEditor.tsx +++ b/src-web/components/editors/GraphQLEditor.tsx @@ -1,12 +1,12 @@ +import { formatSdl } from 'format-graphql'; import { useMemo } from 'react'; +import { useUniqueKey } from '../../hooks/useUniqueKey'; import { Divider } from '../core/Divider'; import type { EditorProps } from '../core/Editor'; import { Editor } from '../core/Editor'; +import { IconButton } from '../core/IconButton'; -type Props = Pick< - EditorProps, - 'heightMode' | 'onChange' | 'defaultValue' | 'className' | 'useTemplating' ->; +type Props = Pick; interface GraphQLBody { query: string; @@ -15,6 +15,7 @@ interface GraphQLBody { } export function GraphQLEditor({ defaultValue, onChange, ...extraEditorProps }: Props) { + const queryKey = useUniqueKey(); const { query, variables } = useMemo(() => { try { const p = JSON.parse(defaultValue ?? '{}'); @@ -36,21 +37,39 @@ export function GraphQLEditor({ defaultValue, onChange, ...extraEditorProps }: P }; const handleChangeVariables = (variables: string) => { - handleChange({ query, variables: JSON.parse(variables) }); + try { + handleChange({ query, variables: JSON.parse(variables) }); + } catch (e) { + // Meh, not much we can do here + } }; return (
- +
+ + { + handleChangeQuery(formatSdl(query)); + setTimeout(queryKey.regenerate, 200); + }} + /> +

Variables

(); const requests = useRequests(); + const { requestId } = useParams<{ requestId?: string }>(); const [activeRequest, setActiveRequest] = useState(null); useEffect(() => { - if (requests.length === 0) { - setActiveRequest(null); - } else { - setActiveRequest(requests.find((r) => r.id === params.requestId) ?? null); - } - }, [requests, params.requestId]); + setActiveRequest(requests.find((r) => r.id === requestId) ?? null); + }, [requests, requestId]); return activeRequest; } diff --git a/src-web/hooks/useActiveWorkspace.ts b/src-web/hooks/useActiveWorkspace.ts index a0515443..b6cb12bf 100644 --- a/src-web/hooks/useActiveWorkspace.ts +++ b/src-web/hooks/useActiveWorkspace.ts @@ -4,17 +4,13 @@ import type { Workspace } from '../lib/models'; import { useWorkspaces } from './useWorkspaces'; export function useActiveWorkspace(): Workspace | null { - const params = useParams<{ workspaceId?: string }>(); const workspaces = useWorkspaces(); + const { workspaceId } = useParams<{ workspaceId?: string }>(); const [activeWorkspace, setActiveWorkspace] = useState(null); useEffect(() => { - if (workspaces.length === 0) { - setActiveWorkspace(null); - } else { - setActiveWorkspace(workspaces.find((w) => w.id === params.workspaceId) ?? null); - } - }, [workspaces, params.workspaceId]); + setActiveWorkspace(workspaces.find((w) => w.id === workspaceId) ?? null); + }, [workspaces, workspaceId]); return activeWorkspace; } diff --git a/src-web/hooks/useCreateRequest.ts b/src-web/hooks/useCreateRequest.ts index e20f61f1..c309f15d 100644 --- a/src-web/hooks/useCreateRequest.ts +++ b/src-web/hooks/useCreateRequest.ts @@ -12,7 +12,7 @@ export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) if (workspace === null) { throw new Error("Cannot create request when there's no active workspace"); } - return invoke('create_request', { ...patch, workspaceId: workspace?.id }); + return invoke('create_request', { ...patch, workspaceId: workspace.id }); }, onSuccess: async (requestId) => { if (navigateAfter) { diff --git a/src-web/hooks/useDeleteRequest.ts b/src-web/hooks/useDeleteRequest.ts index 6e31f5a7..8d6427bc 100644 --- a/src-web/hooks/useDeleteRequest.ts +++ b/src-web/hooks/useDeleteRequest.ts @@ -7,11 +7,11 @@ export function useDeleteRequest(request: HttpRequest | null) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { - if (request == null) return; + if (!request) return; await invoke('delete_request', { requestId: request.id }); }, onSuccess: async () => { - if (request == null) return; + if (!request) return; await queryClient.invalidateQueries(requestsQueryKey(request.workspaceId)); }, }); diff --git a/src-web/hooks/useDeleteResponses.ts b/src-web/hooks/useDeleteResponses.ts index da0e45f5..7804692f 100644 --- a/src-web/hooks/useDeleteResponses.ts +++ b/src-web/hooks/useDeleteResponses.ts @@ -5,11 +5,11 @@ export function useDeleteResponses(requestId?: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { - if (requestId == null) return; + if (!requestId) return; await invoke('delete_all_responses', { requestId }); }, onSuccess: () => { - if (requestId == null) return; + if (!requestId) return; queryClient.setQueryData(['responses', { requestId: requestId }], []); }, }); diff --git a/src-web/hooks/useResponseDelete.ts b/src-web/hooks/useResponseDelete.ts index a208271f..495bd112 100644 --- a/src-web/hooks/useResponseDelete.ts +++ b/src-web/hooks/useResponseDelete.ts @@ -1,6 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import type { HttpResponse } from '../lib/models'; +import { responsesQueryKey } from './useResponses'; export function useDeleteResponse(response: HttpResponse | null) { const queryClient = useQueryClient(); @@ -12,7 +13,7 @@ export function useDeleteResponse(response: HttpResponse | null) { onSuccess: () => { if (response === null) return; queryClient.setQueryData( - ['responses', { requestId: response.requestId }], + responsesQueryKey(response.requestId), (responses: HttpResponse[] = []) => responses.filter((r) => r.id !== response.id), ); }, diff --git a/src-web/hooks/useTheme.ts b/src-web/hooks/useTheme.ts index c4d7a85c..cd0a8221 100644 --- a/src-web/hooks/useTheme.ts +++ b/src-web/hooks/useTheme.ts @@ -8,12 +8,14 @@ import { toggleAppearance, } from '../lib/theme/window'; -const appearanceQueryKey = ['theme', 'appearance']; +export function appearanceQueryKey() { + return ['theme', 'appearance']; +} export function useTheme() { const queryClient = useQueryClient(); const appearance = useQuery({ - queryKey: appearanceQueryKey, + queryKey: appearanceQueryKey(), queryFn: getAppearance, initialData: getAppearance(), }).data; @@ -24,12 +26,10 @@ export function useTheme() { const handleToggleAppearance = async () => { const newAppearance = toggleAppearance(); - await queryClient.setQueryData(appearanceQueryKey, newAppearance); + await queryClient.setQueryData(appearanceQueryKey(), newAppearance); }; - useEffect(() => { - return subscribeToPreferredAppearanceChange(themeChange); - }, []); + useEffect(() => subscribeToPreferredAppearanceChange(themeChange), []); return { appearance, diff --git a/src-web/hooks/useUniqueKey.ts b/src-web/hooks/useUniqueKey.ts new file mode 100644 index 00000000..baf683c1 --- /dev/null +++ b/src-web/hooks/useUniqueKey.ts @@ -0,0 +1,16 @@ +import { useState } from 'react'; + +export function useUniqueKey(len = 10): { key: string; regenerate: () => void } { + const [key, setKey] = useState(() => generate(len)); + return { key, regenerate: () => setKey(generate(len)) }; +} + +const CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + +function generate(len: number): string { + const chars = []; + for (let i = 0; i < len; i++) { + chars.push(CHARS[Math.floor(Math.random() * CHARS.length)]); + } + return chars.join(''); +}