Initial settings implementation

This commit is contained in:
Gregory Schier
2024-01-11 21:13:17 -08:00
parent 11161fda51
commit 202e272e90
18 changed files with 426 additions and 65 deletions

View File

@@ -0,0 +1,33 @@
import { useSettings } from '../hooks/useSettings';
import { useTheme } from '../hooks/useTheme';
import { useUpdateSettings } from '../hooks/useUpdateSettings';
import { Checkbox } from './core/Checkbox';
import { VStack } from './core/Stacks';
export const SettingsDialog = () => {
const { appearance, toggleAppearance } = useTheme();
const settings = useSettings();
const updateSettings = useUpdateSettings();
if (settings == null) {
return null;
}
return (
<VStack space={2}>
<Checkbox
checked={settings.validateCertificates}
title="Validate TLS Certificates"
onChange={(validateCertificates) =>
updateSettings.mutateAsync({ ...settings, validateCertificates })
}
/>
<Checkbox
checked={settings.followRedirects}
title="Follow Redirects"
onChange={(followRedirects) => updateSettings.mutateAsync({ ...settings, followRedirects })}
/>
<Checkbox checked={appearance === 'dark'} title="Dark Mode" onChange={toggleAppearance} />
</VStack>
);
};

View File

@@ -13,6 +13,7 @@ import { IconButton } from './core/IconButton';
import { VStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
import { SettingsDialog } from './SettingsDialog';
export function SettingsDropdown() {
const importData = useImportData();
@@ -61,16 +62,11 @@ export function SettingsDropdown() {
leftSlot: <Icon icon="upload" />,
onSelect: () => exportData.mutate(),
},
{
key: 'appearance',
label: 'Toggle Theme',
onSelect: toggleAppearance,
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
},
{
key: 'hotkeys',
label: 'Keyboard shortcuts',
hotkeyAction: 'hotkeys.showHelp',
leftSlot: <Icon icon="keyboard" />,
onSelect: () => {
dialog.show({
id: 'hotkey-help',
@@ -79,7 +75,20 @@ export function SettingsDropdown() {
render: () => <KeyboardShortcutsDialog />,
});
},
leftSlot: <Icon icon="keyboard" />,
},
{
key: 'settings',
label: 'Settings',
hotkeyAction: 'settings.show',
leftSlot: <Icon icon="gear" />,
onSelect: () => {
dialog.show({
id: 'settings',
size: 'md',
title: 'Settings',
render: () => <SettingsDialog />,
});
},
},
{ type: 'separator', label: `Yaak v${appVersion.data}` },
{

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { useCallback } from 'react';
import { Icon } from './Icon';
import { HStack } from './Stacks';
interface Props {
checked: boolean;
@@ -8,33 +8,47 @@ interface Props {
onChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
hideLabel?: boolean;
}
export function Checkbox({ checked, onChange, className, disabled, title }: Props) {
const handleClick = useCallback(() => {
onChange(!checked);
}, [onChange, checked]);
export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) {
return (
<button
role="checkbox"
aria-checked={checked ? 'true' : 'false'}
disabled={disabled}
onClick={handleClick}
title={title}
className={classNames(
className,
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',
'focus:border-focus',
'disabled:opacity-disabled',
checked && 'bg-gray-200/10',
// Remove focus style
'outline-none',
)}
<HStack
as="label"
space={2}
alignItems="center"
className={classNames(className, disabled && 'opacity-disabled')}
>
<div className="flex items-center justify-center">
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
<div className="relative flex">
<input
aria-hidden
className="appearance-none w-4 h-4 flex-shrink-0 border border-gray-200 rounded focus:border-focus outline-none ring-0"
type="checkbox"
disabled={disabled}
onChange={() => onChange(!checked)}
/>
<div className="absolute inset-0 flex items-center justify-center">
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
</div>
</div>
</button>
{/*<button*/}
{/* role="checkbox"*/}
{/* aria-checked={checked ? 'true' : 'false'}*/}
{/* disabled={disabled}*/}
{/* onClick={handleClick}*/}
{/* title={title}*/}
{/* className={classNames(*/}
{/* className,*/}
{/* 'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',*/}
{/* 'focus:border-focus',*/}
{/* 'disabled:opacity-disabled',*/}
{/* checked && 'bg-gray-200/10',*/}
{/* // Remove focus style*/}
{/* 'outline-none',*/}
{/* )}*/}
{/*>*/}
{/*</button>*/}
{!hideLabel && title}
</HStack>
);
}

View File

@@ -479,11 +479,6 @@ interface MenuItemHotKeyProps {
}
function MenuItemHotKey({ action, onSelect, item }: MenuItemHotKeyProps) {
if (action) {
console.log('MENU ITEM HOTKEY', action, item);
}
useHotKey(action ?? null, () => {
onSelect(item);
});
useHotKey(action ?? null, () => onSelect(item));
return null;
}

View File

@@ -290,13 +290,29 @@ function getExtensions({
// Handle onChange
EditorView.updateListener.of((update) => {
if (onChange && update.docChanged) {
// Only fire onChange if the document changed and the update was from user input. This prevents firing onChange when the document is updated when
// changing pages (one request to another in header editor)
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
onChange.current?.(update.state.doc.toString());
}
}),
];
}
function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
// Make sure document has changed, ensuring user events like selections don't count.
if (viewUpdate.docChanged) {
// Check transactions for any that are direct user input, not changes from Y.js or another extension.
for (const transaction of viewUpdate.transactions) {
// Not using Transaction.isUserEvent because that only checks for a specific User event type ( "input", "delete", etc.). Checking the annotation directly allows for any type of user event.
const userEventType = transaction.annotation(Transaction.userEvent);
if (userEventType) return userEventType;
}
}
return false;
}
const syncGutterBg = ({
parent,
className = '',

View File

@@ -335,7 +335,8 @@ const FormRow = memo(function FormRow({
<span className="w-3" />
)}
<Checkbox
title={pairContainer.pair.enabled ? 'disable entry' : 'Enable item'}
hideLabel
title={pairContainer.pair.enabled ? 'Disable item' : 'Enable item'}
disabled={isLast}
checked={isLast ? false : !!pairContainer.pair.enabled}
className={classNames('mr-2', isLast && '!opacity-disabled')}

View File

@@ -54,7 +54,7 @@ export const VStack = forwardRef(function VStack(
});
type BaseStackProps = HTMLAttributes<HTMLElement> & {
as?: ComponentType | 'ul' | 'form';
as?: ComponentType | 'ul' | 'label' | 'form';
space?: keyof typeof gapClasses;
alignItems?: 'start' | 'center' | 'stretch';
justifyContent?: 'start' | 'center' | 'end' | 'between';

View File

@@ -11,7 +11,8 @@ export type HotkeyAction =
| 'sidebar.focus'
| 'urlBar.focus'
| 'environmentEditor.toggle'
| 'hotkeys.showHelp';
| 'hotkeys.showHelp'
| 'settings.show';
const hotkeys: Record<HotkeyAction, string[]> = {
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
@@ -22,6 +23,7 @@ const hotkeys: Record<HotkeyAction, string[]> = {
'urlBar.focus': ['CmdCtrl+l'],
'environmentEditor.toggle': ['CmdCtrl+e'],
'hotkeys.showHelp': ['CmdCtrl+/'],
'settings.show': ['CmdCtrl+,'],
};
const hotkeyLabels: Record<HotkeyAction, string> = {
@@ -32,7 +34,8 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
'sidebar.focus': 'Focus Sidebar',
'urlBar.focus': 'Focus URL',
'environmentEditor.toggle': 'Edit Environments',
'hotkeys.showHelp': 'Show Hotkeys',
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
'settings.show': 'Open Settings',
};
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];

View File

@@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { Settings } from '../lib/models';
export function settingsQueryKey() {
return ['settings'];
}
export function useSettings() {
return (
useQuery({
queryKey: settingsQueryKey(),
queryFn: async () => {
return (await invoke('get_settings')) as Settings;
},
}).data ?? undefined
);
}

View File

@@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpRequest, Settings } from '../lib/models';
import { requestsQueryKey } from './useRequests';
import { settingsQueryKey } from './useSettings';
export function useUpdateSettings() {
const queryClient = useQueryClient();
return useMutation<void, unknown, Settings>({
mutationFn: async (settings) => {
await invoke('update_settings', { settings });
},
onMutate: async (settings) => {
queryClient.setQueryData<Settings>(settingsQueryKey(), settings);
},
});
}

View File

@@ -9,7 +9,7 @@ export const AUTH_TYPE_NONE = null;
export const AUTH_TYPE_BASIC = 'basic';
export const AUTH_TYPE_BEARER = 'bearer';
export type Model = Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
export type Model = Settings | Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
export interface BaseModel {
readonly id: string;
@@ -17,6 +17,13 @@ export interface BaseModel {
readonly updatedAt: string;
}
export interface Settings extends BaseModel {
readonly model: 'settings';
validateCertificates: boolean;
followRedirects: boolean;
theme: string;
}
export interface Workspace extends BaseModel {
readonly model: 'workspace';
name: string;