mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-22 01:19:13 +01:00
Launch analytics events, changelog, better filter styles
This commit is contained in:
@@ -82,40 +82,43 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
actions={
|
||||
(error || isLoading) && (
|
||||
<Button
|
||||
size="xs"
|
||||
color={error ? 'danger' : 'gray'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'dynamic',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<>
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full mt-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>
|
||||
)
|
||||
error || isLoading
|
||||
? [
|
||||
<Button
|
||||
key="introspection"
|
||||
size="xs"
|
||||
color={error ? 'danger' : 'gray'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'dynamic',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<>
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full mt-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
{...extraEditorProps}
|
||||
/>
|
||||
|
||||
@@ -94,8 +94,8 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
style={style}
|
||||
className={classNames(
|
||||
className,
|
||||
'bg-gray-50 max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1',
|
||||
'dark:bg-gray-100 rounded-md border border-highlight',
|
||||
'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1',
|
||||
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight',
|
||||
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import classNames from 'classnames';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
@@ -6,8 +5,9 @@ import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { Heading } from './core/Heading';
|
||||
import { Input } from './core/Input';
|
||||
import { Select } from './core/Select';
|
||||
import { Separator } from './core/Separator';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
export const SettingsDialog = () => {
|
||||
const workspace = useActiveWorkspace();
|
||||
@@ -20,24 +20,36 @@ export const SettingsDialog = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack space={2}>
|
||||
<HStack className="mt-1" alignItems="center" space={2}>
|
||||
<div className="w-1/3">Appearance</div>
|
||||
<select
|
||||
value={settings.appearance}
|
||||
style={selectBackgroundStyles}
|
||||
onChange={(e) => updateSettings.mutateAsync({ ...settings, appearance: e.target.value })}
|
||||
className={classNames(
|
||||
'font-mono text-xs border w-full px-2 outline-none bg-transparent',
|
||||
'border-highlight focus:border-focus',
|
||||
'h-xs',
|
||||
)}
|
||||
>
|
||||
<option value="system">Match System</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</HStack>
|
||||
<VStack space={2} className="mb-2">
|
||||
<Select
|
||||
name="appearance"
|
||||
label="Appearance"
|
||||
labelPosition="left"
|
||||
labelClassName="w-1/3"
|
||||
size="sm"
|
||||
value={settings.appearance}
|
||||
onChange={(appearance) => updateSettings.mutateAsync({ ...settings, appearance })}
|
||||
options={{
|
||||
system: 'System',
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Select
|
||||
name="updateChannel"
|
||||
label="Update Channel"
|
||||
labelPosition="left"
|
||||
labelClassName="w-1/3"
|
||||
size="sm"
|
||||
value={settings.updateChannel}
|
||||
onChange={(updateChannel) => updateSettings.mutateAsync({ ...settings, updateChannel })}
|
||||
options={{
|
||||
stable: 'Release',
|
||||
beta: 'Early Bird (Beta)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<Heading size={2}>
|
||||
@@ -48,7 +60,7 @@ export const SettingsDialog = () => {
|
||||
</Heading>
|
||||
<VStack className="mt-1 w-full" space={3}>
|
||||
<Input
|
||||
size="xs"
|
||||
size="sm"
|
||||
name="requestTimeout"
|
||||
label="Request Timeout (ms)"
|
||||
labelPosition="left"
|
||||
@@ -75,14 +87,6 @@ export const SettingsDialog = () => {
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
{/*<Checkbox checked={appearance === 'dark'} title="Dark Mode" onChange={toggleAppearance} />*/}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
const selectBackgroundStyles = {
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
||||
backgroundPosition: 'right 0.5rem center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '1.5em 1.5em',
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { invoke, shell } from '@tauri-apps/api';
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useAppVersion } from '../hooks/useAppVersion';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { useUpdateMode } from '../hooks/useUpdateMode';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
@@ -18,18 +18,51 @@ export function SettingsDropdown() {
|
||||
const importData = useImportData();
|
||||
const exportData = useExportData();
|
||||
const appVersion = useAppVersion();
|
||||
const [updateMode, setUpdateMode] = useUpdateMode();
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const dialog = useDialog();
|
||||
const [showChangelog, setShowChangelog] = useState<boolean>(false);
|
||||
|
||||
useListenToTauriEvent('show_changelog', () => {
|
||||
setShowChangelog(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
onClose={() => setShowChangelog(false)}
|
||||
items={[
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
hotKeyAction: 'settings.show',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'settings',
|
||||
size: 'md',
|
||||
title: 'Settings',
|
||||
render: () => <SettingsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard shortcuts',
|
||||
hotKeyAction: 'hotkeys.showHelp',
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'hotkey-help',
|
||||
title: 'Keyboard Shortcuts',
|
||||
size: 'sm',
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'import-data',
|
||||
label: 'Import',
|
||||
leftSlot: <Icon icon="download" />,
|
||||
label: 'Import Data',
|
||||
leftSlot: <Icon icon="folderInput" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
title: 'Import Data',
|
||||
@@ -56,60 +89,41 @@ export function SettingsDropdown() {
|
||||
},
|
||||
{
|
||||
key: 'export-data',
|
||||
label: 'Export',
|
||||
leftSlot: <Icon icon="upload" />,
|
||||
label: 'Export Data',
|
||||
leftSlot: <Icon icon="folderOutput" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard shortcuts',
|
||||
hotKeyAction: 'hotkeys.showHelp',
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'hotkey-help',
|
||||
title: 'Keyboard Shortcuts',
|
||||
size: 'sm',
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
hotKeyAction: 'settings.show',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'settings',
|
||||
size: 'md',
|
||||
title: 'Settings',
|
||||
render: () => <SettingsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appVersion.data}` },
|
||||
{
|
||||
key: 'update-mode',
|
||||
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
||||
onSelect: () => setUpdateMode(updateMode === 'stable' ? 'beta' : 'stable'),
|
||||
leftSlot: <Icon icon="flask" />,
|
||||
},
|
||||
{
|
||||
key: 'update-check',
|
||||
label: 'Check for Updates',
|
||||
onSelect: () => invoke('check_for_updates'),
|
||||
leftSlot: <Icon icon="update" />,
|
||||
onSelect: () => invoke('check_for_updates'),
|
||||
},
|
||||
{
|
||||
key: 'feedback',
|
||||
label: 'Feedback',
|
||||
onSelect: () => shell.open('https://yaak.canny.io'),
|
||||
leftSlot: <Icon icon="chat" />,
|
||||
rightSlot: <Icon icon="externalLink" />,
|
||||
onSelect: () => shell.open('https://yaak.canny.io'),
|
||||
},
|
||||
{
|
||||
key: 'changelog',
|
||||
label: 'Changelog',
|
||||
variant: showChangelog ? 'notify' : 'default',
|
||||
leftSlot: <Icon icon="cake" />,
|
||||
rightSlot: <Icon icon="externalLink" />,
|
||||
onSelect: () => shell.open(`https://yaak.app/changelog/${appVersion.data}`),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton size="sm" title="Main Menu" icon="settings" className="pointer-events-auto" />
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Main Menu"
|
||||
icon="settings"
|
||||
className="pointer-events-auto"
|
||||
showBadge={showChangelog}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { memo } from 'react';
|
||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
@@ -14,7 +15,10 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
return (
|
||||
<HStack>
|
||||
<IconButton
|
||||
onClick={toggle}
|
||||
onClick={() => {
|
||||
trackEvent('Sidebar', 'Toggle');
|
||||
toggle();
|
||||
}}
|
||||
className="pointer-events-auto"
|
||||
size="sm"
|
||||
title="Show sidebar"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import type {
|
||||
@@ -8,7 +7,7 @@ import type {
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useFullscreen, useWindowSize } from 'react-use';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
import React, {
|
||||
Children,
|
||||
@@ -39,7 +40,7 @@ export type DropdownItemDefault = {
|
||||
label: ReactNode;
|
||||
hotKeyAction?: HotkeyAction;
|
||||
hotKeyLabelOnly?: boolean;
|
||||
variant?: 'danger';
|
||||
variant?: 'default' | 'danger' | 'notify';
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
@@ -53,6 +54,8 @@ export interface DropdownProps {
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
items: DropdownItem[];
|
||||
openOnHotKeyAction?: HotkeyAction;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface DropdownRef {
|
||||
@@ -66,14 +69,23 @@ export interface DropdownRef {
|
||||
}
|
||||
|
||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||
{ children, items, openOnHotKeyAction }: DropdownProps,
|
||||
{ children, items, openOnHotKeyAction, onOpen, onClose }: DropdownProps,
|
||||
ref,
|
||||
) {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [isOpen, _setIsOpen] = useState<boolean>(false);
|
||||
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number>();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
|
||||
|
||||
const setIsOpen = useCallback(
|
||||
(o: SetStateAction<boolean>) => {
|
||||
_setIsOpen(o);
|
||||
if (o) onOpen?.();
|
||||
else onClose?.();
|
||||
},
|
||||
[onClose, onOpen],
|
||||
);
|
||||
|
||||
useHotKey(openOnHotKeyAction ?? null, () => {
|
||||
setIsOpen(true);
|
||||
});
|
||||
@@ -112,12 +124,12 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
}),
|
||||
};
|
||||
return cloneElement(existingChild, props);
|
||||
}, [children]);
|
||||
}, [children, setIsOpen]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}, []);
|
||||
}, [setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
|
||||
@@ -307,11 +319,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
|
||||
const docRect = document.documentElement.getBoundingClientRect();
|
||||
const width = triggerShape.right - triggerShape.left;
|
||||
const heightAbove = triggerShape.top;
|
||||
const heightBelow = docRect.height - triggerShape.bottom;
|
||||
const hSpaceRemaining = docRect.width - triggerShape.left;
|
||||
const vSpaceRemaining = docRect.height - triggerShape.bottom;
|
||||
const top = triggerShape?.bottom + 5;
|
||||
const onRight = hSpaceRemaining < 200;
|
||||
const upsideDown = vSpaceRemaining < 200;
|
||||
const upsideDown = heightAbove > heightBelow && heightBelow < 200;
|
||||
const containerStyles = {
|
||||
top: !upsideDown ? top : undefined,
|
||||
bottom: upsideDown ? docRect.height - top : undefined,
|
||||
@@ -462,6 +475,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
|
||||
'focus:bg-highlight focus:text-gray-900 rounded',
|
||||
item.variant === 'danger' && 'text-red-600',
|
||||
item.variant === 'notify' && 'text-pink-600',
|
||||
)}
|
||||
innerClassName="!text-left"
|
||||
{...props}
|
||||
|
||||
@@ -5,7 +5,18 @@ import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/vie
|
||||
import classNames from 'classnames';
|
||||
import { EditorView } from 'codemirror';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
|
||||
import { useActiveWorkspace } from '../../../hooks/useActiveWorkspace';
|
||||
import { IconButton } from '../IconButton';
|
||||
@@ -145,6 +156,12 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [forceUpdateKey]);
|
||||
|
||||
const classList = className?.split(/\s+/) ?? [];
|
||||
const bgClassList = classList
|
||||
.filter((c) => c.match(/(^|:)?bg-.+/)) // Find bg-* classes
|
||||
.map((c) => c.replace(/^bg-/, '!bg-')) // !important
|
||||
.map((c) => c.replace(/^dark:bg-/, 'dark:!bg-')); // !important
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
const initEditorRef = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container === null) {
|
||||
@@ -184,7 +201,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
|
||||
view = new EditorView({ state, parent: container });
|
||||
cm.current = { view, languageCompartment };
|
||||
syncGutterBg({ parent: container, className });
|
||||
syncGutterBg({ parent: container, bgClassList });
|
||||
if (autoFocus) {
|
||||
view.focus();
|
||||
}
|
||||
@@ -198,6 +215,50 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Add bg classes to actions, so they appear over the text
|
||||
const decoratedActions = useMemo(() => {
|
||||
const results = [];
|
||||
const actionClassName = classNames(
|
||||
'transition-opacity opacity-0 group-hover:opacity-50 hover:!opacity-100 shadow',
|
||||
bgClassList,
|
||||
);
|
||||
|
||||
if (format) {
|
||||
results.push(
|
||||
<IconButton
|
||||
showConfirm
|
||||
key="format"
|
||||
size="sm"
|
||||
title="Reformat contents"
|
||||
icon="magicWand"
|
||||
className={classNames(actionClassName)}
|
||||
onClick={() => {
|
||||
if (cm.current === null) return;
|
||||
const { doc } = cm.current.view.state;
|
||||
const formatted = format(doc.toString());
|
||||
// Update editor and blur because the cursor will reset anyway
|
||||
cm.current.view.dispatch({
|
||||
changes: { from: 0, to: doc.length, insert: formatted },
|
||||
});
|
||||
cm.current.view.contentDOM.blur();
|
||||
// Fire change event
|
||||
onChange?.(formatted);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
results.push(
|
||||
Children.map(actions, (existingChild) => {
|
||||
if (!isValidElement(existingChild)) return null;
|
||||
return cloneElement(existingChild, {
|
||||
...existingChild.props,
|
||||
className: classNames(existingChild.props.className, actionClassName),
|
||||
});
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}, [actions, bgClassList, format, onChange]);
|
||||
|
||||
const cmContainer = (
|
||||
<div
|
||||
ref={initEditorRef}
|
||||
@@ -219,7 +280,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
return (
|
||||
<div className="group relative h-full w-full">
|
||||
{cmContainer}
|
||||
{(format || actions) && (
|
||||
{decoratedActions && (
|
||||
<HStack
|
||||
space={1}
|
||||
alignItems="center"
|
||||
@@ -229,28 +290,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
'pointer-events-none', // No pointer events so we don't block the editor
|
||||
)}
|
||||
>
|
||||
{format && (
|
||||
<IconButton
|
||||
showConfirm
|
||||
size="sm"
|
||||
title="Reformat contents"
|
||||
icon="magicWand"
|
||||
className="transition-all opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
if (cm.current === null) return;
|
||||
const { doc } = cm.current.view.state;
|
||||
const formatted = format(doc.toString());
|
||||
// Update editor and blur because the cursor will reset anyway
|
||||
cm.current.view.dispatch({
|
||||
changes: { from: 0, to: doc.length, insert: formatted },
|
||||
});
|
||||
cm.current.view.contentDOM.blur();
|
||||
// Fire change event
|
||||
onChange?.(formatted);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{actions}
|
||||
{decoratedActions}
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
@@ -325,19 +365,14 @@ function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
|
||||
|
||||
const syncGutterBg = ({
|
||||
parent,
|
||||
className = '',
|
||||
bgClassList,
|
||||
}: {
|
||||
parent: HTMLDivElement;
|
||||
className?: string;
|
||||
bgClassList: string[];
|
||||
}) => {
|
||||
const gutterEl = parent.querySelector<HTMLDivElement>('.cm-gutters');
|
||||
const classList = className?.split(/\s+/) ?? [];
|
||||
const bgClasses = classList
|
||||
.filter((c) => c.match(/(^|:)?bg-.+/)) // Find bg-* classes
|
||||
.map((c) => c.replace(/^bg-/, '!bg-')) // !important
|
||||
.map((c) => c.replace(/^dark:bg-/, 'dark:!bg-')); // !important
|
||||
if (gutterEl) {
|
||||
gutterEl?.classList.add(...bgClasses);
|
||||
gutterEl?.classList.add(...bgClassList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { memo } from 'react';
|
||||
const icons = {
|
||||
archive: lucide.ArchiveIcon,
|
||||
box: lucide.BoxIcon,
|
||||
cake: lucide.CakeIcon,
|
||||
chat: lucide.MessageSquare,
|
||||
check: lucide.CheckIcon,
|
||||
chevronDown: lucide.ChevronDownIcon,
|
||||
@@ -13,6 +14,8 @@ const icons = {
|
||||
code: lucide.CodeIcon,
|
||||
copy: lucide.CopyIcon,
|
||||
download: lucide.DownloadIcon,
|
||||
folderInput: lucide.FolderInputIcon,
|
||||
folderOutput: lucide.FolderOutputIcon,
|
||||
externalLink: lucide.ExternalLinkIcon,
|
||||
eye: lucide.EyeIcon,
|
||||
eyeClosed: lucide.EyeOffIcon,
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = IconProps &
|
||||
iconClassName?: string;
|
||||
iconSize?: IconProps['size'];
|
||||
title: string;
|
||||
showBadge?: boolean;
|
||||
};
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
|
||||
@@ -26,6 +27,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
tabIndex,
|
||||
size = 'md',
|
||||
iconSize,
|
||||
showBadge,
|
||||
...props
|
||||
}: Props,
|
||||
ref,
|
||||
@@ -49,7 +51,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
innerClassName="flex items-center justify-center"
|
||||
className={classNames(
|
||||
className,
|
||||
'flex-shrink-0 text-gray-700 hover:text-gray-1000',
|
||||
'relative flex-shrink-0 text-gray-700 hover:text-gray-1000',
|
||||
'!px-0',
|
||||
size === 'md' && 'w-9',
|
||||
size === 'sm' && 'w-8',
|
||||
@@ -58,6 +60,11 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
size={size}
|
||||
{...props}
|
||||
>
|
||||
{showBadge && (
|
||||
<div className="absolute top-0 right-0 w-1/2 h-1/2 flex items-center justify-center">
|
||||
<div className="w-2.5 h-2.5 bg-pink-500 rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
<Icon
|
||||
size={iconSize}
|
||||
icon={confirmed ? 'check' : icon}
|
||||
|
||||
74
src-web/components/core/Select.tsx
Normal file
74
src-web/components/core/Select.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props<T extends string> {
|
||||
name: string;
|
||||
label: string;
|
||||
labelPosition?: 'top' | 'left';
|
||||
labelClassName?: string;
|
||||
hideLabel?: boolean;
|
||||
value: string;
|
||||
options: Record<T, string>;
|
||||
onChange: (value: T) => void;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function Select<T extends string>({
|
||||
labelPosition = 'top',
|
||||
name,
|
||||
labelClassName,
|
||||
hideLabel,
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
size = 'md',
|
||||
}: Props<T>) {
|
||||
const id = `input-${name}`;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'pointer-events-auto', // Just in case we're placing in disabled parent
|
||||
labelPosition === 'left' && 'flex items-center gap-2',
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
)}
|
||||
>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={classNames(
|
||||
labelClassName,
|
||||
'text-sm text-gray-900 whitespace-nowrap',
|
||||
hideLabel && 'sr-only',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<select
|
||||
value={value}
|
||||
style={selectBackgroundStyles}
|
||||
onChange={(e) => onChange(e.target.value as T)}
|
||||
className={classNames(
|
||||
'font-mono text-xs border w-full px-2 outline-none bg-transparent',
|
||||
'border-highlight focus:border-focus',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'sm' && 'h-sm',
|
||||
size === 'md' && 'h-md',
|
||||
size === 'lg' && 'h-lg',
|
||||
)}
|
||||
>
|
||||
{Object.entries<string>(options).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectBackgroundStyles = {
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
||||
backgroundPosition: 'right 0.5rem center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '1.5em 1.5em',
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDebouncedSetState } from '../../hooks/useDebouncedSetState';
|
||||
import { useFilterResponse } from '../../hooks/useFilterResponse';
|
||||
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
||||
@@ -9,7 +11,6 @@ import type { HttpResponse } from '../../lib/models';
|
||||
import { Editor } from '../core/Editor';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { Input } from '../core/Input';
|
||||
import { HStack } from '../core/Stacks';
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
@@ -34,30 +35,44 @@ export function TextViewer({ response, pretty }: Props) {
|
||||
const isJson = contentType?.includes('json');
|
||||
const isXml = contentType?.includes('xml') || contentType?.includes('html');
|
||||
const canFilter = isJson || isXml;
|
||||
const actions = canFilter && (
|
||||
<HStack className="w-full" justifyContent="end" space={1}>
|
||||
{isSearching && (
|
||||
<Input
|
||||
hideLabel
|
||||
autoFocus
|
||||
containerClassName="bg-gray-50"
|
||||
size="sm"
|
||||
placeholder={isJson ? 'JSONPath expression' : 'XPath expression'}
|
||||
label="Filter expression"
|
||||
name="filter"
|
||||
defaultValue={filterText}
|
||||
onKeyDown={(e) => e.key === 'Escape' && clearSearch()}
|
||||
onChange={setDebouncedFilterText}
|
||||
/>
|
||||
)}
|
||||
|
||||
const actions = useMemo<ReactNode[]>(() => {
|
||||
const result: ReactNode[] = [];
|
||||
|
||||
if (!canFilter) return result;
|
||||
|
||||
if (isSearching) {
|
||||
result.push(
|
||||
<div key="input" className="w-full !opacity-100">
|
||||
<Input
|
||||
hideLabel
|
||||
autoFocus
|
||||
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' && clearSearch()}
|
||||
onChange={setDebouncedFilterText}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
result.push(
|
||||
<IconButton
|
||||
key="icon"
|
||||
size="sm"
|
||||
icon={isSearching ? 'x' : 'filter'}
|
||||
title={isSearching ? 'Close filter' : 'Filter response'}
|
||||
onClick={clearSearch}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
className={classNames(isSearching && '!opacity-100')}
|
||||
/>,
|
||||
);
|
||||
|
||||
return result;
|
||||
}, [canFilter, clearSearch, filterText, isJson, isSearching, setDebouncedFilterText]);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
|
||||
Reference in New Issue
Block a user