mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-18 08:37:48 +01:00
Compare commits
34 Commits
v2024.5.0
...
v2024.5.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2383e8468f | ||
|
|
a79d485b6a | ||
|
|
7eb931d689 | ||
|
|
995cd2aa7b | ||
|
|
1ce50e0c1b | ||
|
|
bce3d26a1a | ||
|
|
d9680ad0fa | ||
|
|
e2e026e1ff | ||
|
|
16739d9a37 | ||
|
|
524a4f2275 | ||
|
|
ba66883dc2 | ||
|
|
2caa735a2e | ||
|
|
90637fda6b | ||
|
|
2cef46b46a | ||
|
|
14b3abf76c | ||
|
|
8cd3961f87 | ||
|
|
5eb2e2b5a2 | ||
|
|
5dd897e042 | ||
|
|
9c77ec296d | ||
|
|
696e72323b | ||
|
|
3e8c01f436 | ||
|
|
80fc4dec09 | ||
|
|
671885fc8c | ||
|
|
002b61f0d7 | ||
|
|
d32b462bd9 | ||
|
|
5c8b47288a | ||
|
|
8e662e6feb | ||
|
|
83aaeb94f6 | ||
|
|
8606940dee | ||
|
|
57d548743f | ||
|
|
dab7ee2492 | ||
|
|
9360fd7e43 | ||
|
|
0f7969d10a | ||
|
|
6b373b5985 |
@@ -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]}`;
|
||||
|
||||
@@ -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 ?? ""
|
||||
} : {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -281,7 +281,7 @@ function SidebarButton({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete-environment',
|
||||
key: 'delete',
|
||||
variant: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" size="sm" />,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,8 +12,6 @@ export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }:
|
||||
<PairEditor
|
||||
valueAutocompleteVariables
|
||||
nameAutocompleteVariables
|
||||
namePlaceholder="param_name"
|
||||
valuePlaceholder="Value"
|
||||
pairs={urlParameters}
|
||||
onChange={onChange}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
|
||||
|
||||
@@ -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(/.*/);
|
||||
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
‎
|
||||
{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] ?? '';
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,6 @@ export function useFilterResponse({
|
||||
|
||||
return (await invoke('cmd_filter_response', { responseId, filter })) as string | null;
|
||||
},
|
||||
}).data ?? ''
|
||||
}).data ?? null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user