2024.5.0 (#39)

This commit is contained in:
Gregory Schier
2024-06-03 14:08:24 -07:00
committed by GitHub
parent 60e469a1c9
commit 4f9a7e9c88
197 changed files with 12283 additions and 3505 deletions
+2 -2
View File
@@ -32,10 +32,10 @@ export function Confirm({ onHide, onResult, confirmText, variant = 'confirm' }:
return (
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button className="focus" color={colors[variant]} onClick={handleSuccess}>
<Button color={colors[variant]} onClick={handleSuccess}>
{confirmText ?? confirmButtonTexts[variant]}
</Button>
<Button className="focus" color="gray" onClick={handleHide}>
<Button onClick={handleHide} variant="border">
Cancel
</Button>
</HStack>
+2 -2
View File
@@ -52,10 +52,10 @@ export function Prompt({
onChange={setValue}
/>
<HStack space={2} justifyContent="end">
<Button className="focus" color="gray" onClick={onHide}>
<Button onClick={onHide} variant="border">
Cancel
</Button>
<Button type="submit" className="focus" color="primary">
<Button type="submit" color="primary">
{confirmLabel}
</Button>
</HStack>
+1 -1
View File
@@ -16,5 +16,5 @@ export function useAppInfo() {
const metadata = await invoke('cmd_metadata');
return metadata as AppInfo;
},
});
}).data;
}
+8 -5
View File
@@ -1,9 +1,9 @@
import { useNavigate } from 'react-router-dom';
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveRequestId } from './useActiveRequestId';
import type { Environment } from '../lib/models';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import type { Environment } from '../lib/models';
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironmentId';
import { useActiveRequestId } from './useActiveRequestId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
export type RouteParamsWorkspace = {
workspaceId: string;
@@ -18,6 +18,9 @@ export const routePaths = {
workspaces() {
return '/workspaces';
},
workspaceSettings({ workspaceId } = { workspaceId: ':workspaceId' } as RouteParamsWorkspace) {
return `/workspaces/${workspaceId}/settings`;
},
workspace(
{ workspaceId, environmentId } = {
workspaceId: ':workspaceId',
+8 -1
View File
@@ -1,5 +1,6 @@
import { readText, writeText } from '@tauri-apps/plugin-clipboard-manager';
import { useCallback, useEffect } from 'react';
import { useToast } from '../components/ToastContext';
import { useWindowFocus } from './useWindowFocus';
import { createGlobalState } from 'react-use';
@@ -8,6 +9,7 @@ const useClipboardTextState = createGlobalState<string>('');
export function useClipboardText() {
const [value, setValue] = useClipboardTextState();
const focused = useWindowFocus();
const toast = useToast();
useEffect(() => {
readText().then(setValue);
@@ -16,9 +18,14 @@ export function useClipboardText() {
const setText = useCallback(
(text: string) => {
writeText(text).catch(console.error);
toast.show({
id: 'copied',
variant: 'copied',
message: 'Copied to clipboard',
});
setValue(text);
},
[setValue],
[setValue, toast],
);
return [value, setText] as const;
+1 -1
View File
@@ -8,7 +8,7 @@ export function useCommandPalette() {
const appInfo = useAppInfo();
useHotKey('command_palette.toggle', () => {
// Disabled in production for now
if (!appInfo.data?.isDev) {
if (!appInfo?.isDev) {
return;
}
+7 -19
View File
@@ -1,23 +1,11 @@
import { invoke } from '@tauri-apps/api/core';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { useState } from 'react';
import { useToast } from '../components/ToastContext';
import { useClipboardText } from './useClipboardText';
export function useCopyAsCurl(requestId: string) {
const [checked, setChecked] = useState<boolean>(false);
const toast = useToast();
return [
checked,
async () => {
const cmd: string = await invoke('cmd_request_to_curl', { requestId });
await writeText(cmd);
setChecked(true);
setTimeout(() => setChecked(false), 800);
toast.show({
variant: 'copied',
message: 'Curl copied to clipboard',
});
return cmd;
},
] as const;
const [, copy] = useClipboardText();
return async () => {
const cmd: string = await invoke('cmd_request_to_curl', { requestId });
copy(cmd);
return cmd;
};
}
+1 -1
View File
@@ -18,6 +18,6 @@ export function useFilterResponse({
return (await invoke('cmd_filter_response', { responseId, filter })) as string | null;
},
}).data ?? null
}).data ?? ''
);
}
+11 -1
View File
@@ -20,7 +20,10 @@ export type HotkeyAction =
| 'sidebar.focus'
| 'sidebar.toggle'
| 'urlBar.focus'
| 'command_palette.toggle';
| 'command_palette.toggle'
| 'app.zoom_in'
| 'app.zoom_out'
| 'app.zoom_reset';
const hotkeys: Record<HotkeyAction, string[]> = {
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
@@ -37,6 +40,9 @@ const hotkeys: Record<HotkeyAction, string[]> = {
'sidebar.toggle': ['CmdCtrl+b'],
'urlBar.focus': ['CmdCtrl+l'],
'command_palette.toggle': ['CmdCtrl+k'],
'app.zoom_in': ['CmdCtrl+='],
'app.zoom_out': ['CmdCtrl+-'],
'app.zoom_reset': ['CmdCtrl+0'],
};
const hotkeyLabels: Record<HotkeyAction, string> = {
@@ -54,6 +60,9 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
'sidebar.toggle': 'Toggle Sidebar',
'urlBar.focus': 'Focus URL',
'command_palette.toggle': 'Toggle Command Palette',
'app.zoom_in': 'Zoom In',
'app.zoom_out': 'Zoom Out',
'app.zoom_reset': 'Zoom to Actual Size',
};
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
@@ -114,6 +123,7 @@ export function useHotKey(
e.preventDefault();
e.stopPropagation();
callbackRef.current(e);
currentKeys.current.clear();
}
}
}
+14 -18
View File
@@ -1,26 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { useEffect, useState } from 'react';
import { useWindowSize } from 'react-use';
import { useDebouncedValue } from './useDebouncedValue';
export function useIsFullscreen() {
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const windowSize = useWindowSize();
const debouncedWindowWidth = useDebouncedValue(windowSize.width);
useEffect(() => {
(async function () {
// Fullscreen state isn't updated right after resize event on Mac (needs to wait for animation) so
// we'll poll for 10 seconds to see if it changes. Hopefully Tauri eventually adds a way to listen
// for this.
for (let i = 0; i < 100; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
const newIsFullscreen = await getCurrent().isFullscreen();
if (newIsFullscreen !== isFullscreen) {
setIsFullscreen(newIsFullscreen);
break;
}
}
})();
}, [windowSize, isFullscreen]);
// NOTE: Fullscreen state isn't updated right after resize event on Mac (needs to wait for animation) so
// we'll wait for a bit using the debounced window size. Hopefully Tauri eventually adds a way to listen
// for fullscreen change events.
return isFullscreen;
return (
useQuery({
queryKey: ['is_fullscreen', debouncedWindowWidth],
queryFn: async () => {
return getCurrent().isFullscreen();
},
}).data ?? false
);
}
+4 -3
View File
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { buildKeyValueKey, getKeyValue, setKeyValue } from '../lib/keyValueStore';
const DEFAULT_NAMESPACE = 'app';
const DEFAULT_NAMESPACE = 'global';
export function keyValueQueryKey({
namespace = DEFAULT_NAMESPACE,
@@ -20,7 +20,7 @@ export function useKeyValue<T extends Object | null>({
key,
fallback,
}: {
namespace?: 'app' | 'no_sync' | 'global';
namespace?: 'global' | 'no_sync';
key: string | string[];
fallback: T;
}) {
@@ -51,7 +51,8 @@ export function useKeyValue<T extends Object | null>({
await mutate.mutateAsync(value);
}
},
[fallback, key, mutate, namespace],
// eslint-disable-next-line react-hooks/exhaustive-deps
[fallback, key, namespace],
);
const reset = useCallback(async () => mutate.mutateAsync(fallback), [mutate, fallback]);
+4 -4
View File
@@ -1,8 +1,8 @@
import { invoke } from '@tauri-apps/api/core';
import { open } from '@tauri-apps/plugin-shell';
import { Button } from '../components/core/Button';
import { useToast } from '../components/ToastContext';
import { useListenToTauriEvent } from './useListenToTauriEvent';
import { Button } from '../components/core/Button';
import { open } from '@tauri-apps/plugin-shell';
import { invoke } from '@tauri-apps/api/core';
export function useNotificationToast() {
const toast = useToast();
@@ -31,7 +31,7 @@ export function useNotificationToast() {
actionLabel && actionUrl ? (
<Button
size="xs"
color="gray"
color="secondary"
className="mr-auto min-w-[5rem]"
onClick={() => {
toast.hide(payload.id);
+19
View File
@@ -0,0 +1,19 @@
import type { GrpcConnection, GrpcRequest } from '../lib/models';
import { useGrpcConnections } from './useGrpcConnections';
import { useKeyValue } from './useKeyValue';
import { useLatestGrpcConnection } from './useLatestGrpcConnection';
export function usePinnedGrpcConnection(activeRequest: GrpcRequest) {
const latestConnection = useLatestGrpcConnection(activeRequest.id);
const { set: setPinnedConnectionId, value: pinnedConnectionId } = useKeyValue<string | null>({
// Key on latest connection instead of activeRequest because connections change out of band of active request
key: ['pinned_grpc_connection_id', latestConnection?.id ?? 'n/a'],
fallback: null,
namespace: 'global',
});
const connections = useGrpcConnections(activeRequest.id);
const activeConnection: GrpcConnection | null =
connections.find((r) => r.id === pinnedConnectionId) ?? latestConnection;
return { activeConnection, setPinnedConnectionId, pinnedConnectionId, connections } as const;
}
+10 -19
View File
@@ -1,28 +1,19 @@
import { useCallback, useEffect } from 'react';
import { createGlobalState } from 'react-use';
import type { HttpRequest, HttpResponse } from '../lib/models';
import { useHttpResponses } from './useHttpResponses';
import { useKeyValue } from './useKeyValue';
import { useLatestHttpResponse } from './useLatestHttpResponse';
const usePinnedResponseIdState = createGlobalState<string | null>(null);
export function usePinnedHttpResponse(activeRequest: HttpRequest) {
const [pinnedResponseId, setPinnedResponseId] = usePinnedResponseIdState();
const latestResponse = useLatestHttpResponse(activeRequest.id);
const { set: setPinnedResponseId, value: pinnedResponseId } = useKeyValue<string | null>({
// Key on latest response instead of activeRequest because responses change out of band of active request
key: ['pinned_http_response_id', latestResponse?.id ?? 'n/a'],
fallback: null,
namespace: 'global',
});
const responses = useHttpResponses(activeRequest.id);
const activeResponse: HttpResponse | null = pinnedResponseId
? responses.find((r) => r.id === pinnedResponseId) ?? null
: latestResponse ?? null;
const activeResponse: HttpResponse | null =
responses.find((r) => r.id === pinnedResponseId) ?? latestResponse;
// Unset pinned response when a new one comes in
useEffect(() => setPinnedResponseId(null), [responses.length, setPinnedResponseId]);
const setPinnedResponse = useCallback(
(r: HttpResponse) => {
setPinnedResponseId(r.id);
},
[setPinnedResponseId],
);
return { activeResponse, setPinnedResponse, pinnedResponseId, responses } as const;
return { activeResponse, setPinnedResponseId, pinnedResponseId, responses } as const;
}
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useState } from 'react';
import {
getCSSAppearance,
getWindowAppearance,
subscribeToWindowAppearanceChange,
} from '../lib/theme/appearance';
import { type Appearance } from '../lib/theme/window';
export function usePreferredAppearance() {
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(getCSSAppearance());
useEffect(() => {
getWindowAppearance().then(setPreferredAppearance);
return subscribeToWindowAppearanceChange(setPreferredAppearance);
}, []);
return preferredAppearance;
}
+14
View File
@@ -0,0 +1,14 @@
import { usePreferredAppearance } from './usePreferredAppearance';
import { useSettings } from './useSettings';
export function useResolvedAppearance() {
const preferredAppearance = usePreferredAppearance();
const settings = useSettings();
const appearance =
settings == null || settings?.appearance === 'system'
? preferredAppearance
: settings.appearance;
return appearance;
}
+20
View File
@@ -0,0 +1,20 @@
import { isThemeDark } from '../lib/theme/window';
import { useResolvedAppearance } from './useResolvedAppearance';
import { useSettings } from './useSettings';
import { useThemes } from './useThemes';
export function useResolvedTheme() {
const appearance = useResolvedAppearance();
const settings = useSettings();
const { themes, fallback } = useThemes();
const darkThemes = themes.filter((t) => isThemeDark(t));
const lightThemes = themes.filter((t) => !isThemeDark(t));
const dark = darkThemes.find((t) => t.id === settings?.themeDark) ?? fallback.dark;
const light = lightThemes.find((t) => t.id === settings?.themeLight) ?? fallback.light;
const active = appearance === 'dark' ? dark : light;
return { dark, light, active };
}
-35
View File
@@ -1,35 +0,0 @@
import { useEffect, useState } from 'react';
import type { Appearance } from '../lib/theme/window';
import {
setAppearanceOnDocument,
getPreferredAppearance,
subscribeToPreferredAppearanceChange,
} from '../lib/theme/window';
import { useSettings } from './useSettings';
export function useSyncAppearance() {
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(
getPreferredAppearance(),
);
const settings = useSettings();
// Set appearance when preferred theme changes
useEffect(() => {
return subscribeToPreferredAppearanceChange(setPreferredAppearance);
}, []);
const appearance =
settings == null || settings?.appearance === 'system'
? preferredAppearance
: settings.appearance;
useEffect(() => {
if (settings == null) {
return;
}
setAppearanceOnDocument(settings.appearance as Appearance);
}, [appearance, settings]);
return { appearance };
}
+23
View File
@@ -0,0 +1,23 @@
import { emit } from '@tauri-apps/api/event';
import { useEffect } from 'react';
import type { YaakTheme } from '../lib/theme/window';
import { addThemeStylesToDocument, setThemeOnDocument } from '../lib/theme/window';
import { useResolvedTheme } from './useResolvedTheme';
export function useSyncThemeToDocument() {
const theme = useResolvedTheme();
useEffect(() => {
setThemeOnDocument(theme.active);
emitBgChange(theme.active);
}, [theme.active]);
useEffect(() => {
addThemeStylesToDocument(theme.active);
}, [theme.active]);
}
function emitBgChange(t: YaakTheme) {
if (t.background == null) return;
emit('yaak_bg_changed', t.background.hex()).catch(console.error);
}
-23
View File
@@ -1,23 +0,0 @@
// import { useEffect } from 'react';
// import { fallbackRequestName } from '../lib/fallbackRequestName';
// import { useActiveEnvironment } from './useActiveEnvironment';
// import { useActiveRequest } from './useActiveRequest';
// import { useActiveWorkspace } from './useActiveWorkspace';
export function useSyncWindowTitle() {
// const activeRequest = useActiveRequest();
// const activeWorkspace = useActiveWorkspace();
// const activeEnvironment = useActiveEnvironment();
// useEffect(() => {
// let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak';
// if (activeEnvironment) {
// newTitle += ` [${activeEnvironment.name}]`;
// }
// if (activeRequest) {
// newTitle += ` ${fallbackRequestName(activeRequest)}`;
// }
//
// // TODO: This resets the stoplight position so we can't use it yet
// // getCurrent().setTitle(newTitle).catch(console.error);
// }, [activeEnvironment, activeRequest, activeWorkspace]);
}
@@ -0,0 +1,38 @@
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useOsInfo } from './useOsInfo';
import { emit } from '@tauri-apps/api/event';
export function useSyncWorkspaceRequestTitle() {
const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace();
const activeEnvironment = useActiveEnvironment();
const osInfo = useOsInfo();
useEffect(() => {
if (osInfo?.osType == null) {
return;
}
let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak';
if (activeEnvironment) {
newTitle += ` [${activeEnvironment.name}]`;
}
if (activeRequest) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
newTitle += ` ${fallbackRequestName(activeRequest)}`;
}
// TODO: This resets the stoplight position so we can't use it on macOS yet. So we send
// a custom command instead
if (osInfo?.osType !== 'macos') {
getCurrent().setTitle(newTitle).catch(console.error);
} else {
emit('yaak_title_changed', newTitle).catch(console.error);
}
}, [activeEnvironment, activeRequest, activeWorkspace, osInfo?.osType]);
}
+13
View File
@@ -0,0 +1,13 @@
import { defaultDarkTheme, defaultLightTheme, yaakThemes } from '../lib/theme/themes';
export function useThemes() {
const dark = defaultDarkTheme;
const light = defaultLightTheme;
const otherThemes = yaakThemes
.filter((t) => t.id !== dark.id && t.id !== light.id)
.sort((a, b) => a.name.localeCompare(b.name));
const themes = [dark, light, ...otherThemes];
return { themes, fallback: { dark, light } };
}
+8 -9
View File
@@ -1,17 +1,16 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import type { Settings } from '../lib/models';
import { settingsQueryKey } from './useSettings';
import { useSettings } from './useSettings';
export function useUpdateSettings() {
const queryClient = useQueryClient();
const settings = useSettings();
return useMutation<void, unknown, Settings>({
mutationFn: async (settings) => {
await invoke('cmd_update_settings', { settings });
},
onMutate: async (settings) => {
queryClient.setQueryData<Settings[]>(settingsQueryKey(), [settings]);
return useMutation<void, unknown, Partial<Settings>>({
mutationFn: async (patch) => {
if (settings == null) return;
const newSettings: Settings = { ...settings, ...patch };
await invoke('cmd_update_settings', { settings: newSettings });
},
});
}
+28
View File
@@ -0,0 +1,28 @@
import { useCallback } from 'react';
import { useSettings } from './useSettings';
import { useUpdateSettings } from './useUpdateSettings';
export function useZoom() {
const settings = useSettings();
const updateSettings = useUpdateSettings();
const zoomIn = useCallback(() => {
if (!settings) return;
updateSettings.mutate({
interfaceScale: Math.min(1.8, settings.interfaceScale * 1.1),
});
}, [settings, updateSettings]);
const zoomOut = useCallback(() => {
if (!settings) return;
updateSettings.mutate({
interfaceScale: Math.max(0.4, settings.interfaceScale * 0.9),
});
}, [settings, updateSettings]);
const zoomReset = useCallback(() => {
updateSettings.mutate({ ...settings, interfaceScale: 1 });
}, [settings, updateSettings]);
return { zoomIn, zoomOut, zoomReset };
}