Compare commits

..

34 Commits

Author SHA1 Message Date
Gregory Schier
2383e8468f Also release on beta branch 2024-05-31 10:37:21 -07:00
Gregory Schier
a79d485b6a Bump version 2024-05-31 09:46:57 -07:00
Gregory Schier
7eb931d689 Fix multipart files 2024-05-31 09:32:16 -07:00
Gregory Schier
995cd2aa7b Fade in window contents (Layout) 2024-05-31 09:16:52 -07:00
Gregory Schier
1ce50e0c1b Window border on Linux 2024-05-31 08:43:35 -07:00
Gregory Schier
bce3d26a1a Try tweaking settings/theme for Windows. Setting the theme didn't work. 2024-05-30 23:38:49 -07:00
Gregory Schier
d9680ad0fa Ignore stoplights when centering settings title 2024-05-30 12:33:59 -07:00
Gregory Schier
e2e026e1ff Tweak appearance settings 2024-05-30 12:32:12 -07:00
Gregory Schier
16739d9a37 Dracula theme 2024-05-30 11:56:57 -07:00
Gregory Schier
524a4f2275 Tweak 2024-05-30 11:45:40 -07:00
Gregory Schier
ba66883dc2 Theme analytics and Moonlight 2024-05-30 11:00:50 -07:00
Gregory Schier
2caa735a2e Tweak settings for release 2024-05-30 10:28:59 -07:00
Gregory Schier
90637fda6b Unlisten to window event and fix hotkeys 2024-05-30 00:29:01 -07:00
Gregory Schier
2cef46b46a Window title working again 2024-05-30 00:11:55 -07:00
Gregory Schier
14b3abf76c Fix window stoplights 2024-05-29 22:59:34 -07:00
Gregory Schier
8cd3961f87 Custom font sizes and better zoom 2024-05-29 12:10:01 -07:00
Gregory Schier
5eb2e2b5a2 Merge branch 'refs/heads/release'
# Conflicts:
#	src-tauri/src/updates.rs
2024-05-26 09:11:29 -07:00
Gregory Schier
5dd897e042 Initial handler for yaak:// protocol 2024-05-26 08:45:15 -07:00
Gregory Schier
9c77ec296d Better theme export 2024-05-24 18:54:30 -07:00
Gregory Schier
696e72323b More theme stuff 2024-05-24 17:36:48 -07:00
Gregory Schier
3e8c01f436 Tweak themes 2024-05-23 10:16:29 -07:00
Gregory Schier
80fc4dec09 Hotdog stand 2024-05-23 10:14:07 -07:00
Gregory Schier
671885fc8c More themes! 2024-05-23 09:40:11 -07:00
Gregory Schier
002b61f0d7 Set window title on Mac 2024-05-23 09:40:00 -07:00
Gregory Schier
d32b462bd9 More tweaks 2024-05-22 23:44:47 -07:00
Gregory Schier
5c8b47288a Tweak Yaak default themes 2024-05-22 23:28:53 -07:00
Gregory Schier
8e662e6feb A bunch more theme stuff 2024-05-22 23:14:53 -07:00
Gregory Schier
83aaeb94f6 Theme system refactor (#31) 2024-05-21 17:56:06 -07:00
Gregory Schier
8606940dee Move is_dev check for updates 2024-05-16 10:28:25 -07:00
Gregory Schier
57d548743f Merge remote-tracking branch 'refs/remotes/origin/release'
# Conflicts:
#	src-tauri/tauri.conf.json
2024-05-14 15:35:33 -07:00
Gregory Schier
dab7ee2492 Bump version 2024-05-14 15:34:34 -07:00
Gregory Schier
9360fd7e43 Oops 2024-05-14 15:33:27 -07:00
Gregory Schier
0f7969d10a Fix maximize permission 2024-05-14 14:54:09 -07:00
Gregory Schier
6b373b5985 Fix autocomplete 2024-05-14 14:47:33 -07:00
29 changed files with 184 additions and 242 deletions

View File

@@ -180,7 +180,6 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
f.src != null
? {
enabled: !f.disabled,
contentType: f.contentType ?? null,
name: f.key ?? '',
file: f.src ?? '',
}
@@ -245,7 +244,6 @@ function convertTemplateSyntax<T>(obj: T): T {
}
const idCount: Partial<Record<Model['model'], number>> = {};
function generateId(model: Model['model']): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;

View File

@@ -131,7 +131,6 @@ function j(e) {
form: b(t.formdata).map(
(n) => n.src != null ? {
enabled: !n.disabled,
contentType: n.contentType ?? null,
name: n.key ?? "",
file: n.src ?? ""
} : {

View File

@@ -89,24 +89,14 @@ pub async fn send_http_request(
let uri = match http::Uri::from_str(url_string.as_str()) {
Ok(u) => u,
Err(e) => {
return response_err(
response,
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await;
return response_err(response, e.to_string(), window).await;
}
};
// Yes, we're parsing both URI and URL because they could return different errors
let url = match Url::from_str(uri.to_string().as_str()) {
Ok(u) => u,
Err(e) => {
return response_err(
response,
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await;
return response_err(response, e.to_string(), window).await;
}
};

View File

@@ -752,16 +752,16 @@ async fn cmd_import_data(
) -> Result<WorkspaceExportResources, String> {
let mut result: Option<ImportResult> = None;
let plugins = vec![
"importer-postman",
"importer-insomnia",
"importer-yaak",
"importer-insomnia",
"importer-postman",
"importer-curl",
];
let file = read_to_string(file_path)
let file = fs::read_to_string(file_path)
.unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
for plugin_name in plugins {
let v = run_plugin_import(&w.app_handle(), plugin_name, file_contents)
let v = plugin::run_plugin_import(&w.app_handle(), plugin_name, file_contents)
.await
.map_err(|e| e.to_string())?;
if let Some(r) = v {

View File

@@ -6,7 +6,7 @@
"frontendDist": "../dist"
},
"productName": "Yaak",
"version": "2024.5.0",
"version": "2024.5.0-beta.1",
"identifier": "app.yaak.desktop",
"app": {
"withGlobalTauri": false,

View File

@@ -281,7 +281,7 @@ function SidebarButton({
},
},
{
key: 'delete-environment',
key: 'delete',
variant: 'danger',
label: 'Delete',
leftSlot: <Icon icon="trash" size="sm" />,

View File

@@ -30,8 +30,6 @@ export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props)
<PairEditor
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="entry_name"
valuePlaceholder="Value"
pairs={pairs}
onChange={handleChange}
forceUpdateKey={forceUpdateKey}

View File

@@ -4,7 +4,6 @@ import { useEffect } from 'react';
import { useClipboardText } from '../hooks/useClipboardText';
import { useCommandPalette } from '../hooks/useCommandPalette';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { environmentsQueryKey } from '../hooks/useEnvironments';
import { foldersQueryKey } from '../hooks/useFolders';
import { useGlobalCommands } from '../hooks/useGlobalCommands';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
@@ -63,8 +62,6 @@ export function GlobalHooks() {
? httpResponsesQueryKey(model)
: model.model === 'folder'
? foldersQueryKey(model)
: model.model === 'environment'
? environmentsQueryKey(model)
: model.model === 'grpc_connection'
? grpcConnectionsQueryKey(model)
: model.model === 'grpc_event'
@@ -99,8 +96,10 @@ export function GlobalHooks() {
queryClient.setQueryData<Model[]>(queryKey, (values = []) => {
const index = values.findIndex((v) => modelsEq(v, model)) ?? -1;
if (index >= 0) {
// console.log('UPDATED', payload);
return [...values.slice(0, index), model, ...values.slice(index + 1)];
} else {
// console.log('CREATED', payload);
return pushToFront ? [model, ...(values ?? [])] : [...(values ?? []), model];
}
});
@@ -118,8 +117,6 @@ export function GlobalHooks() {
queryClient.setQueryData(httpResponsesQueryKey(model), removeById(model));
} else if (model.model === 'folder') {
queryClient.setQueryData(foldersQueryKey(model), removeById(model));
} else if (model.model === 'environment') {
queryClient.setQueryData(environmentsQueryKey(model), removeById(model));
} else if (model.model === 'grpc_request') {
queryClient.setQueryData(grpcRequestsQueryKey(model), removeById(model));
} else if (model.model === 'grpc_connection') {

View File

@@ -109,14 +109,11 @@ export const RequestPane = memo(function RequestPane({
if (bodyType === BODY_TYPE_NONE) {
newContentType = null;
} else if (
activeRequest.method.toLowerCase() !== 'put' &&
activeRequest.method.toLowerCase() !== 'patch' &&
activeRequest.method.toLowerCase() !== 'post' &&
(bodyType === BODY_TYPE_FORM_URLENCODED ||
bodyType === BODY_TYPE_FORM_MULTIPART ||
bodyType === BODY_TYPE_JSON ||
bodyType === BODY_TYPE_OTHER ||
bodyType === BODY_TYPE_XML)
bodyType === BODY_TYPE_FORM_URLENCODED ||
bodyType === BODY_TYPE_FORM_MULTIPART ||
bodyType === BODY_TYPE_JSON ||
bodyType === BODY_TYPE_OTHER ||
bodyType === BODY_TYPE_XML
) {
patch.method = 'POST';
newContentType = bodyType === BODY_TYPE_OTHER ? 'text/plain' : bodyType;
@@ -184,7 +181,6 @@ export const RequestPane = memo(function RequestPane({
activeRequest.authenticationType,
activeRequest.bodyType,
activeRequest.headers,
activeRequest.method,
activeRequest.urlParameters,
handleContentTypeChange,
updateRequest,

View File

@@ -14,7 +14,7 @@ import { Editor } from '../core/Editor';
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import type { SelectProps } from '../core/Select';
import type { SelectOption } from '../core/Select';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { HStack, VStack } from '../core/Stacks';
@@ -64,14 +64,14 @@ export function SettingsAppearance() {
return null;
}
const lightThemes: SelectProps<string>['options'] = themes
const lightThemes: SelectOption<string>[] = themes
.filter((theme) => !isThemeDark(theme))
.map((theme) => ({
label: theme.name,
value: theme.id,
}));
const darkThemes: SelectProps<string>['options'] = themes
const darkThemes: SelectOption<string>[] = themes
.filter((theme) => isThemeDark(theme))
.map((theme) => ({
label: theme.name,
@@ -109,7 +109,7 @@ export function SettingsAppearance() {
<Select
name="appearance"
label="Appearance"
labelPosition="top"
labelPosition="left"
size="sm"
value={settings.appearance}
onChange={(appearance) => {
@@ -123,6 +123,7 @@ export function SettingsAppearance() {
]}
/>
<HStack space={2}>
<div>Theme</div>
{(settings.appearance === 'system' || settings.appearance === 'light') && (
<Select
hideLabel
@@ -130,9 +131,8 @@ export function SettingsAppearance() {
name="lightTheme"
label="Light Theme"
size="sm"
className="flex-1"
value={activeTheme.light.id}
options={lightThemes}
options={[{ label: 'Light Mode Themes', options: lightThemes }]}
onChange={(themeLight) => {
trackEvent('theme', 'update', { theme: themeLight, appearance: 'light' });
updateSettings.mutateAsync({ ...settings, themeLight });
@@ -143,12 +143,11 @@ export function SettingsAppearance() {
<Select
hideLabel
name="darkTheme"
className="flex-1"
label="Dark Theme"
leftSlot={<Icon icon="moon" />}
size="sm"
value={activeTheme.dark.id}
options={darkThemes}
options={[{ label: 'Dark Mode Themes', options: darkThemes }]}
onChange={(themeDark) => {
trackEvent('theme', 'update', { theme: themeDark, appearance: 'dark' });
updateSettings.mutateAsync({ ...settings, themeDark });

View File

@@ -8,8 +8,8 @@ import { useUpdateWorkspace } from '../../hooks/useUpdateWorkspace';
import { Checkbox } from '../core/Checkbox';
import { Heading } from '../core/Heading';
import { IconButton } from '../core/IconButton';
import { Input } from '../core/Input';
import { KeyValueRow, KeyValueRows } from '../core/KeyValueRow';
import { PlainInput } from '../core/PlainInput';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { VStack } from '../core/Stacks';
@@ -59,7 +59,7 @@ export function SettingsGeneral() {
</div>
</Heading>
<VStack className="mt-1 w-full" space={3}>
<PlainInput
<Input
size="sm"
name="requestTimeout"
label="Request Timeout (ms)"
@@ -68,7 +68,6 @@ export function SettingsGeneral() {
defaultValue={`${workspace.settingRequestTimeout}`}
validate={(value) => parseInt(value) >= 0}
onChange={(v) => updateWorkspace.mutate({ settingRequestTimeout: parseInt(v) || 0 })}
type="number"
/>
<Checkbox

View File

@@ -12,8 +12,6 @@ export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }:
<PairEditor
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="param_name"
valuePlaceholder="Value"
pairs={urlParameters}
onChange={onChange}
forceUpdateKey={forceUpdateKey}

View File

@@ -120,7 +120,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
{isLoading ? (
<Icon icon="refresh" size={size} className="animate-spin mr-1" />
) : leftSlot ? (
<div className="mr-2">{leftSlot}</div>
<div className="mr-1">{leftSlot}</div>
) : null}
<div
className={classNames(

View File

@@ -59,7 +59,6 @@ export interface DropdownProps {
items: DropdownItem[];
onOpen?: () => void;
onClose?: () => void;
fullWidth?: boolean;
hotKeyAction?: HotkeyAction;
}
@@ -74,7 +73,7 @@ export interface DropdownRef {
}
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
{ children, items, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps,
{ children, items, onOpen, onClose, hotKeyAction }: DropdownProps,
ref,
) {
const [isOpen, _setIsOpen] = useState<boolean>(false);
@@ -154,7 +153,6 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
<Menu
ref={menuRef}
showTriangle
fullWidth={fullWidth}
defaultSelectedIndex={defaultSelectedIndex}
items={items}
triggerShape={triggerRect ?? null}
@@ -205,7 +203,6 @@ interface MenuProps {
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'> | null;
onClose: () => void;
showTriangle?: boolean;
fullWidth?: boolean;
isOpen: boolean;
}
@@ -214,7 +211,6 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
className,
isOpen,
items,
fullWidth,
onClose,
triggerShape,
defaultSelectedIndex,
@@ -363,23 +359,21 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
const heightAbove = triggerShape.top;
const heightBelow = docRect.height - triggerShape.bottom;
const hSpaceRemaining = docRect.width - triggerShape.left;
const top = triggerShape.bottom + 5;
const top = triggerShape?.bottom + 5;
const onRight = hSpaceRemaining < 200;
const upsideDown = heightAbove > heightBelow && heightBelow < 200;
const triggerWidth = triggerShape.right - triggerShape.left;
const containerStyles = {
top: !upsideDown ? top : undefined,
bottom: upsideDown ? docRect.height - top : undefined,
right: onRight ? docRect.width - triggerShape.right : undefined,
left: !onRight ? triggerShape.left : undefined,
minWidth: fullWidth ? triggerWidth : undefined,
right: onRight ? docRect.width - triggerShape?.right : undefined,
left: !onRight ? triggerShape?.left : undefined,
};
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
const triangleStyles = onRight
? { right: width / 2, marginRight: '-0.2rem', ...size }
: { left: width / 2, marginLeft: '-0.2rem', ...size };
return { containerStyles, triangleStyles };
}, [fullWidth, triggerShape]);
}, [triggerShape]);
const filteredItems = useMemo(
() => items.filter((i) => getNodeText(i.label).toLowerCase().includes(filter.toLowerCase())),
@@ -527,7 +521,9 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
onClick={handleClick}
justify="start"
leftSlot={
item.leftSlot && <div className="pr-2 flex justify-start opacity-70">{item.leftSlot}</div>
item.leftSlot && (
<div className="pr-2 flex justify-start text-fg-subtle">{item.leftSlot}</div>
)
}
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
innerClassName="!text-left"

View File

@@ -266,7 +266,7 @@
}
&.cm-completionInfo-right {
@apply ml-1 -mt-0.5 font-sans;
@apply ml-1 -mt-0.5;
}
&.cm-completionInfo-right-narrow {
@@ -278,7 +278,7 @@
}
&.cm-tooltip-autocomplete {
@apply font-mono;
@apply font-mono text-editor;
& > ul {
@apply p-1 max-h-[40vh];

View File

@@ -195,7 +195,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
placeholderCompartment.current.of(
placeholderExt(placeholderElFromText(placeholder ?? '')),
),
wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []),
wrapLinesCompartment.current.of([]),
...getExtensions({
container,
readOnly,
@@ -331,25 +331,7 @@ function getExtensions({
undefined;
return [
...baseExtensions, // Must be first
tooltips({ parent }),
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
...(singleLine ? [singleLineExt()] : []),
...(!singleLine ? [multiLineExtensions] : []),
...(readOnly
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
: []),
// ------------------------ //
// Things that must be last //
// ------------------------ //
EditorView.updateListener.of((update) => {
if (onChange && update.docChanged) {
onChange.current?.(update.state.doc.toString());
}
}),
// NOTE: These *must* be anonymous functions so the references update properly
EditorView.domEventHandlers({
focus: () => {
onFocus.current?.();
@@ -364,6 +346,22 @@ function getExtensions({
onPaste.current?.(e.clipboardData?.getData('text/plain') ?? '');
},
}),
tooltips({ parent }),
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
...(singleLine ? [singleLineExt()] : []),
...(!singleLine ? [multiLineExtensions] : []),
...(readOnly
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
: []),
// Handle onChange
EditorView.updateListener.of((update) => {
if (onChange && update.docChanged) {
onChange.current?.(update.state.doc.toString());
}
}),
...baseExtensions,
];
}

View File

@@ -39,7 +39,7 @@ import { text } from './text/extension';
import { twig } from './twig/extension';
import { url } from './url/extension';
export const syntaxHighlightStyle = HighlightStyle.define([
export const myHighlightStyle = HighlightStyle.define([
{
tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment],
color: 'var(--fg-subtler)',
@@ -61,7 +61,7 @@ export const syntaxHighlightStyle = HighlightStyle.define([
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--fg-danger)' },
]);
const syntaxTheme = EditorView.theme({}, { dark: true });
const myTheme = EditorView.theme({}, { dark: true });
const syntaxExtensions: Record<string, LanguageSupport> = {
'application/graphql': graphqlLanguageSupport(),
@@ -108,8 +108,8 @@ export const baseExtensions = [
return (a.boost ?? 0) - (b.boost ?? 0);
},
}),
syntaxHighlighting(syntaxHighlightStyle),
syntaxTheme,
syntaxHighlighting(myHighlightStyle),
myTheme,
EditorState.allowMultipleSelections.of(true),
];

View File

@@ -20,11 +20,7 @@ export interface GenericCompletionConfig {
/**
* Complete options, always matching until the start of the line
*/
export function genericCompletion(config?: GenericCompletionConfig) {
if (config == null) return [];
const { minMatch = 1, options } = config;
export function genericCompletion({ options, minMatch = 1 }: GenericCompletionConfig) {
return function completions(context: CompletionContext) {
const toMatch = context.matchBefore(/.*/);

View File

@@ -1,13 +1,13 @@
import type { LanguageSupport } from '@codemirror/language';
import { LRLanguage } from '@codemirror/language';
import { parseMixed } from '@lezer/common';
import type { Environment, Workspace } from '../../../../lib/models';
import type { GenericCompletionConfig } from '../genericCompletion';
import { genericCompletion } from '../genericCompletion';
import { placeholders } from './placeholder';
import { textLanguageName } from '../text/extension';
import { twigCompletion } from './completion';
import { placeholders } from './placeholder';
import { parser as twigParser } from './twig';
import type { Environment, Workspace } from '../../../../lib/models';
export function twig(
base: LanguageSupport,
@@ -15,19 +15,25 @@ export function twig(
workspace: Workspace | null,
autocomplete?: GenericCompletionConfig,
) {
const language = mixLanguage(base);
const allVariables = [...(workspace?.variables ?? []), ...(environment?.variables ?? [])];
const variables = allVariables.filter((v) => v.enabled) ?? [];
const variables =
[...(workspace?.variables ?? []), ...(environment?.variables ?? [])].filter((v) => v.enabled) ??
[];
const completions = twigCompletion({ options: variables });
const language = mixLanguage(base);
const completion = language.data.of({ autocomplete: completions });
const completionBase = base.language.data.of({ autocomplete: completions });
const additionalCompletion = autocomplete
? [base.language.data.of({ autocomplete: genericCompletion(autocomplete) })]
: [];
return [
language,
completion,
completionBase,
base.support,
placeholders(variables),
language.data.of({ autocomplete: completions }),
base.language.data.of({ autocomplete: completions }),
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
base.language.data.of({ autocomplete: genericCompletion(autocomplete) }),
...additionalCompletion,
];
}

View File

@@ -9,7 +9,7 @@ export function FormattedError({ children }: Props) {
return (
<pre
className={classNames(
'w-full select-auto cursor-text bg-background-highlight-secondary p-3 rounded',
'w-full select-auto cursor-text bg-gray-100 p-3 rounded',
'whitespace-pre-wrap border border-fg-danger border-dashed overflow-x-auto',
)}
>

View File

@@ -133,11 +133,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
>
<label
htmlFor={id}
className={classNames(
labelClassName,
'text-fg-subtle whitespace-nowrap',
hideLabel && 'sr-only',
)}
className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
>
{label}
</label>

View File

@@ -389,7 +389,7 @@ function PairEditorRow({
<Button
size="xs"
color="secondary"
className="font-mono text-2xs rtl"
className="font-mono text-sm"
onClick={async (e) => {
e.preventDefault();
const selected = await open({
@@ -403,9 +403,7 @@ function PairEditorRow({
handleChangeValueFile(selected.path);
}}
>
{/* Special character to insert ltr text in rtl element without making things wonky */}
&#x200E;
{pairContainer.pair.value || 'Select File'}
{getFileName(pairContainer.pair.value) || 'Select File'}
</Button>
) : (
<Input
@@ -496,3 +494,9 @@ const newPairContainer = (initialPair?: Pair): PairContainer => {
const pair = initialPair ?? { name: '', value: '', enabled: true, isFile: false };
return { id, pair };
};
const getFileName = (path: string | null | undefined): string => {
if (typeof path !== 'string') return '';
const parts = path.split(/[\\/]/);
return parts[parts.length - 1] ?? '';
};

View File

@@ -85,11 +85,7 @@ export const PlainInput = forwardRef<HTMLInputElement, PlainInputProps>(function
>
<label
htmlFor={id}
className={classNames(
labelClassName,
'text-fg-subtle whitespace-nowrap',
hideLabel && 'sr-only',
)}
className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
>
{label}
</label>

View File

@@ -50,9 +50,5 @@ export function RadioDropdown<T = string | null>({
[items, extraItems, value, onChange],
);
return (
<Dropdown fullWidth items={dropdownItems}>
{children}
</Dropdown>
);
return <Dropdown items={dropdownItems}>{children}</Dropdown>;
}

View File

@@ -1,14 +1,9 @@
import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react';
import { useState } from 'react';
import { useOsInfo } from '../../hooks/useOsInfo';
import type { ButtonProps } from './Button';
import { Button } from './Button';
import type { RadioDropdownItem } from './RadioDropdown';
import { RadioDropdown } from './RadioDropdown';
import { HStack } from './Stacks';
export interface SelectProps<T extends string> {
interface Props<T extends string> {
name: string;
label: string;
labelPosition?: 'top' | 'left';
@@ -16,12 +11,22 @@ export interface SelectProps<T extends string> {
hideLabel?: boolean;
value: T;
leftSlot?: ReactNode;
options: RadioDropdownItem<T>[];
options: SelectOption<T>[] | SelectOptionGroup<T>[];
onChange: (value: T) => void;
size?: ButtonProps['size'];
size?: 'xs' | 'sm' | 'md' | 'lg';
className?: string;
}
export interface SelectOption<T extends string> {
label: string;
value: T;
}
export interface SelectOptionGroup<T extends string> {
label: string;
options: SelectOption<T>[];
}
export function Select<T extends string>({
labelPosition = 'top',
name,
@@ -34,8 +39,7 @@ export function Select<T extends string>({
onChange,
className,
size = 'md',
}: SelectProps<T>) {
const osInfo = useOsInfo();
}: Props<T>) {
const [focused, setFocused] = useState<boolean>(false);
const id = `input-${name}`;
return (
@@ -45,68 +49,55 @@ export function Select<T extends string>({
'x-theme-input',
'w-full',
'pointer-events-auto', // Just in case we're placing in disabled parent
labelPosition === 'left' && 'grid grid-cols-[auto_1fr] items-center gap-2',
labelPosition === 'left' && 'flex items-center gap-2',
labelPosition === 'top' && 'flex-row gap-0.5',
)}
>
<label
htmlFor={id}
className={classNames(
labelClassName,
'text-fg-subtle whitespace-nowrap',
hideLabel && 'sr-only',
)}
className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
>
{label}
</label>
{osInfo?.osType === 'macos' ? (
<HStack
space={2}
className={classNames(
'w-full rounded-md text-fg text-sm font-mono',
'pl-2',
'border',
focused ? 'border-border-focus' : 'border-background-highlight',
size === 'xs' && 'h-xs',
size === 'sm' && 'h-sm',
size === 'md' && 'h-md',
)}
<HStack
space={2}
className={classNames(
'w-full rounded-md text-fg text-sm font-mono',
'pl-2',
'border',
focused ? 'border-border-focus' : 'border-background-highlight',
size === 'xs' && 'h-xs',
size === 'sm' && 'h-sm',
size === 'md' && 'h-md',
size === 'lg' && 'h-lg',
)}
>
{leftSlot && <div>{leftSlot}</div>}
<select
value={value}
style={selectBackgroundStyles}
onChange={(e) => onChange(e.target.value as T)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
className={classNames('pr-7 w-full outline-none bg-transparent')}
>
{leftSlot && <div>{leftSlot}</div>}
<select
value={value}
style={selectBackgroundStyles}
onChange={(e) => onChange(e.target.value as T)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
className={classNames('pr-7 w-full outline-none bg-transparent')}
>
{options.map((o) => {
if (o.type === 'separator') return null;
return (
<option key={o.label} value={o.value}>
{o.label}
</option>
);
})}
</select>
</HStack>
) : (
// Use custom "select" component until Tauri can be configured to have select menus not always appear in
// light mode
<RadioDropdown value={value} onChange={onChange} items={options}>
<Button
className="w-full text-sm font-mono"
justify="start"
variant="border"
size={size}
leftSlot={leftSlot}
forDropdown
>
{options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'}
</Button>
</RadioDropdown>
)}
{options.map((o) =>
'options' in o ? (
<optgroup key={o.label} label={o.label}>
{o.options.map(({ label, value }) => (
<option key={label} value={value}>
{label}
</option>
))}
</optgroup>
) : (
<option key={o.label} value={o.value}>
{o.label}
</option>
),
)}
</select>
</HStack>
</div>
);
}

View File

@@ -1,11 +1,11 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import { createGlobalState } from 'react-use';
import { useContentTypeFromHeaders } from '../../hooks/useContentTypeFromHeaders';
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useDebouncedState } from '../../hooks/useDebouncedState';
import { useFilterResponse } from '../../hooks/useFilterResponse';
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
import { useToggle } from '../../hooks/useToggle';
import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
import type { HttpResponse } from '../../lib/models';
import { Editor } from '../core/Editor';
@@ -21,42 +21,25 @@ interface Props {
className?: string;
}
const useFilterText = createGlobalState<Record<string, string | null>>({});
export function TextViewer({ response, pretty, className }: Props) {
const [filterTextMap, setFilterTextMap] = useFilterText();
const filterText = filterTextMap[response.id] ?? null;
const debouncedFilterText = useDebouncedValue(filterText, 300);
const setFilterText = useCallback(
(v: string | null) => {
setFilterTextMap((m) => ({ ...m, [response.id]: v }));
},
[setFilterTextMap, response],
);
const [isSearching, toggleIsSearching] = useToggle();
const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState<string>('', 400);
const contentType = useContentTypeFromHeaders(response.headers);
const rawBody = useResponseBodyText(response) ?? '';
const isSearching = filterText != null;
const formattedBody =
pretty && contentType?.includes('json')
? tryFormatJson(rawBody)
: pretty && contentType?.includes('xml')
? tryFormatXml(rawBody)
: rawBody;
const filteredResponse = useFilterResponse({ filter: filterText, responseId: response.id });
const filteredResponse = useFilterResponse({
filter: debouncedFilterText ?? '',
responseId: response.id,
});
const body = isSearching && filterText?.length > 0 ? filteredResponse : formattedBody;
const toggleSearch = useCallback(() => {
if (isSearching) {
setFilterText(null);
} else {
setFilterText('');
}
}, [isSearching, setFilterText]);
const body = filteredResponse ?? formattedBody;
const clearSearch = useCallback(() => {
toggleIsSearching();
setFilterText('');
}, [setFilterText, toggleIsSearching]);
const isJson = contentType?.includes('json');
const isXml = contentType?.includes('xml') || contentType?.includes('html');
@@ -71,17 +54,16 @@ export function TextViewer({ response, pretty, className }: Props) {
result.push(
<div key="input" className="w-full !opacity-100">
<Input
key={response.id}
hideLabel
autoFocus
containerClassName="bg-background"
containerClassName="bg-gray-100 dark:bg-gray-50"
size="sm"
placeholder={isJson ? 'JSONPath expression' : 'XPath expression'}
label="Filter expression"
name="filter"
defaultValue={filterText}
onKeyDown={(e) => e.key === 'Escape' && toggleSearch()}
onChange={setFilterText}
onKeyDown={(e) => e.key === 'Escape' && clearSearch()}
onChange={setDebouncedFilterText}
/>
</div>,
);
@@ -93,16 +75,13 @@ export function TextViewer({ response, pretty, className }: Props) {
size="sm"
icon={isSearching ? 'x' : 'filter'}
title={isSearching ? 'Close filter' : 'Filter response'}
onClick={toggleSearch}
className={classNames(
'bg-background border !border-background-highlight',
isSearching && '!opacity-100',
)}
onClick={clearSearch}
className={classNames(isSearching && '!opacity-100')}
/>,
);
return result;
}, [canFilter, filterText, isJson, isSearching, setFilterText, toggleSearch]);
}, [canFilter, clearSearch, filterText, isJson, isSearching, setDebouncedFilterText]);
return (
<Editor

View File

@@ -18,6 +18,6 @@ export function useFilterResponse({
return (await invoke('cmd_filter_response', { responseId, filter })) as string | null;
},
}).data ?? ''
}).data ?? null
);
}

View File

@@ -1,22 +1,26 @@
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);
// 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.
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]);
return (
useQuery({
queryKey: ['is_fullscreen', debouncedWindowWidth],
queryFn: async () => {
return getCurrent().isFullscreen();
},
}).data ?? false
);
return isFullscreen;
}

View File

@@ -40,10 +40,12 @@ export const colors = {
const moonlightDefault: YaakTheme = {
id: 'moonlight',
name: 'Moonlight',
background: new Color('#222436', 'dark'),
foreground: new Color('#d5def8', 'dark'),
foregroundSubtle: new Color('#828bb8', 'dark'),
foregroundSubtler: new Color('hsl(232,26%,43%)', 'dark'),
background: new Color(colors.gray4, 'dark'),
backgroundHighlight: new Color(colors.gray5, 'dark'),
backgroundHighlightSecondary: new Color(colors.gray5, 'dark'),
foreground: new Color(colors.gray11, 'dark'),
foregroundSubtle: new Color(colors.gray7, 'dark'),
foregroundSubtler: new Color(colors.gray6, 'dark'),
colors: {
primary: new Color(colors.purple, 'dark'),
secondary: new Color(colors.desaturatedGray, 'dark'),
@@ -56,9 +58,13 @@ const moonlightDefault: YaakTheme = {
components: {
appHeader: {
background: new Color(colors.gray3, 'dark'),
backgroundHighlight: new Color(colors.gray5, 'dark'),
backgroundHighlightSecondary: new Color(colors.gray4, 'dark'),
},
sidebar: {
background: new Color(colors.gray3, 'dark'),
backgroundHighlight: new Color(colors.gray5, 'dark'),
backgroundHighlightSecondary: new Color(colors.gray4, 'dark'),
},
},
};