Better project selector, Fixes #2, and a bunch more

This commit is contained in:
Gregory Schier
2023-10-26 09:11:44 -07:00
parent 2f64f45aba
commit 2a29c4b551
19 changed files with 126 additions and 86 deletions

View File

@@ -58,8 +58,9 @@
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react": "^3.1.0",
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e", "@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tauri-apps/cli": "^1.5.6", "@tauri-apps/cli": "^1.5.4",
"@types/node": "^18.7.10", "@types/node": "^18.7.10",
"@types/papaparse": "^5.3.7", "@types/papaparse": "^5.3.7",
"@types/parse-color": "^1.0.1", "@types/parse-color": "^1.0.1",
@@ -69,7 +70,6 @@
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0", "@typescript-eslint/parser": "^5.57.0",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"eslint": "^8.34.0", "eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^8.6.0",

View File

@@ -498,17 +498,7 @@ async fn list_environments(
.await .await
.expect("Failed to find environments"); .expect("Failed to find environments");
println!(""); Ok(environments)
if environments.is_empty() {
println!("CREATING DEFAULT ENVIRONMENT");
let data: HashMap<String, JsonValue> = HashMap::new();
let environment = models::create_environment(workspace_id, "Default", data, pool)
.await
.expect("Failed to create default environment");
Ok(vec![environment])
} else {
Ok(environments)
}
} }
#[tauri::command] #[tauri::command]

View File

@@ -4,7 +4,7 @@ import { keyValueQueryKey } from '../hooks/useKeyValue';
import { requestsQueryKey } from '../hooks/useRequests'; import { requestsQueryKey } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { responsesQueryKey } from '../hooks/useResponses'; import { responsesQueryKey } from '../hooks/useResponses';
import { useTauriEvent } from '../hooks/useTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { DEFAULT_FONT_SIZE } from '../lib/constants'; import { DEFAULT_FONT_SIZE } from '../lib/constants';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore'; import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
@@ -15,7 +15,7 @@ export function GlobalHooks() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null); const { wasUpdatedExternally } = useRequestUpdateKey(null);
useTauriEvent<Model>('created_model', ({ payload, windowLabel }) => { useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return; if (shouldIgnoreEvent(payload, windowLabel)) return;
const queryKey = const queryKey =
@@ -40,7 +40,7 @@ export function GlobalHooks() {
} }
}); });
useTauriEvent<Model>('updated_model', ({ payload, windowLabel }) => { useListenToTauriEvent<Model>('updated_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return; if (shouldIgnoreEvent(payload, windowLabel)) return;
const queryKey = const queryKey =
@@ -70,7 +70,7 @@ export function GlobalHooks() {
} }
}); });
useTauriEvent<Model>('deleted_model', ({ payload, windowLabel }) => { useListenToTauriEvent<Model>('deleted_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return; if (shouldIgnoreEvent(payload, windowLabel)) return;
if (shouldIgnoreModel(payload)) return; if (shouldIgnoreModel(payload)) return;
@@ -85,7 +85,7 @@ export function GlobalHooks() {
queryClient.setQueryData(keyValueQueryKey(payload), undefined); queryClient.setQueryData(keyValueQueryKey(payload), undefined);
} }
}); });
useTauriEvent<number>('zoom', ({ payload: zoomDelta, windowLabel }) => { useListenToTauriEvent<number>('zoom', ({ payload: zoomDelta, windowLabel }) => {
if (windowLabel !== appWindow.label) return; if (windowLabel !== appWindow.label) return;
const fontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize); const fontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize);

View File

@@ -2,12 +2,12 @@ import type { HTMLAttributes, ReactElement } from 'react';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest'; import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { useTauriEvent } from '../hooks/useTauriEvent';
import { useTheme } from '../hooks/useTheme'; import { useTheme } from '../hooks/useTheme';
import type { DropdownRef } from './core/Dropdown'; import type { DropdownRef } from './core/Dropdown';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import { HotKey } from './core/HotKey'; import { HotKey } from './core/HotKey';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
interface Props { interface Props {
requestId: string; requestId: string;
@@ -20,12 +20,12 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
const dropdownRef = useRef<DropdownRef>(null); const dropdownRef = useRef<DropdownRef>(null);
const { appearance, toggleAppearance } = useTheme(); const { appearance, toggleAppearance } = useTheme();
useTauriEvent('toggle_settings', () => { useListenToTauriEvent('toggle_settings', () => {
dropdownRef.current?.toggle(); dropdownRef.current?.toggle();
}); });
// TODO: Put this somewhere better // TODO: Put this somewhere better
useTauriEvent('duplicate_request', () => { useListenToTauriEvent('duplicate_request', () => {
duplicateRequest.mutate(); duplicateRequest.mutate();
}); });

View File

@@ -6,7 +6,7 @@ import { memo, useCallback, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useTauriEvent } from '../hooks/useTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader, HttpRequest } from '../lib/models'; import type { HttpHeader, HttpRequest } from '../lib/models';
@@ -140,7 +140,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
[updateRequest], [updateRequest],
); );
useTauriEvent( useListenToTauriEvent(
'send_request', 'send_request',
async ({ windowLabel }) => { async ({ windowLabel }) => {
if (windowLabel !== appWindow.label) return; if (windowLabel !== appWindow.label) return;

View File

@@ -10,16 +10,18 @@ import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
import { useLatestResponse } from '../hooks/useLatestResponse'; import { useLatestResponse } from '../hooks/useLatestResponse';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useTauriEvent } from '../hooks/useTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest'; import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { isResponseLoading } from '../lib/models'; import { isResponseLoading } from '../lib/models';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag'; import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker'; import { DropMarker } from './DropMarker';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId'; import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
import { IconButton } from './core/IconButton';
interface Props { interface Props {
className?: string; className?: string;
@@ -63,7 +65,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
routes.navigate('request', { routes.navigate('request', {
requestId, requestId,
workspaceId: request.workspaceId, workspaceId: request.workspaceId,
environmentId: activeEnvironmentId, environmentId: activeEnvironmentId ?? undefined,
}); });
setSelectedIndex(index); setSelectedIndex(index);
focusActiveRequest(index); focusActiveRequest(index);
@@ -93,7 +95,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
useKeyPressEvent('Backspace', handleDeleteKey); useKeyPressEvent('Backspace', handleDeleteKey);
useKeyPressEvent('Delete', handleDeleteKey); useKeyPressEvent('Delete', handleDeleteKey);
useTauriEvent( useListenToTauriEvent(
'focus_sidebar', 'focus_sidebar',
() => { () => {
if (hidden || hasFocus) return; if (hidden || hasFocus) return;
@@ -149,11 +151,22 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
tabIndex={hidden ? -1 : 0} tabIndex={hidden ? -1 : 0}
className={classNames(className, 'h-full relative grid grid-rows-[minmax(0,1fr)_auto]')} className={classNames(
className,
'h-full relative grid grid-rows-[auto_minmax(0,1fr)_auto]',
)}
> >
<HStack className="mt-1 mb-2 pt-1 mx-2" justifyContent="between" alignItems="center" space={1}>
<WorkspaceActionsDropdown
forDropdown={false}
className="text-left mb-0"
justify="start"
/>
<IconButton size="sm" icon="plusCircle" title="Create Request" />
</HStack>
<VStack <VStack
as="ul" as="ul"
className="relative py-3 overflow-y-auto overflow-x-visible" className="relative pb-3 overflow-y-auto overflow-x-visible"
draggable={false} draggable={false}
> >
<SidebarItems <SidebarItems

View File

@@ -1,7 +1,7 @@
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useCreateRequest } from '../hooks/useCreateRequest'; import { useCreateRequest } from '../hooks/useCreateRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useTauriEvent } from '../hooks/useTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
export const SidebarActions = memo(function SidebarActions() { export const SidebarActions = memo(function SidebarActions() {
@@ -12,7 +12,7 @@ export const SidebarActions = memo(function SidebarActions() {
createRequest.mutate({}); createRequest.mutate({});
}, [createRequest]); }, [createRequest]);
useTauriEvent('new_request', () => { useListenToTauriEvent('new_request', () => {
createRequest.mutate({}); createRequest.mutate({});
}); });

View File

@@ -5,7 +5,7 @@ import { memo, useCallback, useRef } from 'react';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading'; import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendRequest } from '../hooks/useSendRequest'; import { useSendRequest } from '../hooks/useSendRequest';
import { useTauriEvent } from '../hooks/useTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
@@ -39,7 +39,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
[sendRequest], [sendRequest],
); );
useTauriEvent('focus_url', () => { useListenToTauriEvent('focus_url', () => {
inputRef.current?.focus(); inputRef.current?.focus();
}); });

View File

@@ -6,12 +6,12 @@ import type {
MouseEvent as ReactMouseEvent, MouseEvent as ReactMouseEvent,
ReactNode, ReactNode,
} from 'react'; } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useWindowSize } from 'react-use'; import { useWindowSize } from 'react-use';
import { useOsInfo } from '../hooks/useOsInfo'; import { useOsInfo } from '../hooks/useOsInfo';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useSidebarWidth } from '../hooks/useSidebarWidth'; import { useSidebarWidth } from '../hooks/useSidebarWidth';
import { useTauriEvent } from '../hooks/useTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { WINDOW_FLOATING_SIDEBAR_WIDTH } from '../lib/constants'; import { WINDOW_FLOATING_SIDEBAR_WIDTH } from '../lib/constants';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
@@ -38,7 +38,7 @@ export default function Workspace() {
null, null,
); );
useTauriEvent('toggle_sidebar', toggle); useListenToTauriEvent('toggle_sidebar', toggle);
// float/un-float sidebar on window resize // float/un-float sidebar on window resize
useEffect(() => { useEffect(() => {

View File

@@ -9,6 +9,7 @@ import { usePrompt } from '../hooks/usePrompt';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace'; import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { useWorkspaces } from '../hooks/useWorkspaces';
import { Button } from './core/Button'; import { Button } from './core/Button';
import type { ButtonProps } from './core/Button';
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
@@ -17,12 +18,11 @@ import { HStack } from './core/Stacks';
import { useDialog } from './DialogContext'; import { useDialog } from './DialogContext';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId'; import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
type Props = { type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown'>;
className?: string;
};
export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
className, className,
...buttonProps
}: Props) { }: Props) {
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
@@ -36,9 +36,10 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const routes = useAppRoutes(); const routes = useAppRoutes();
const items: DropdownItem[] = useMemo(() => { const items: DropdownItem[] = useMemo(() => {
const workspaceItems = workspaces.map((w) => ({ const workspaceItems: DropdownItem[] = workspaces.map((w) => ({
key: w.id, key: w.id,
label: w.name, label: w.name,
rightSlot: w.id === activeWorkspaceId ? <Icon icon="check" /> : undefined,
onSelect: async () => { onSelect: async () => {
dialog.show({ dialog.show({
id: 'open-workspace', id: 'open-workspace',
@@ -147,6 +148,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
]; ];
}, [ }, [
activeWorkspace?.name, activeWorkspace?.name,
activeWorkspaceId,
createWorkspace, createWorkspace,
deleteWorkspace.mutate, deleteWorkspace.mutate,
dialog, dialog,
@@ -163,6 +165,8 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
size="sm" size="sm"
className={classNames(className, 'text-gray-800 !px-2 truncate')} className={classNames(className, 'text-gray-800 !px-2 truncate')}
forDropdown forDropdown
leftSlot={<img src="https://yaak.app/logo.svg" alt="Workspace logo" className="w-4 h-4 mr-1" />}
{...buttonProps}
> >
{activeWorkspace?.name} {activeWorkspace?.name}
</Button> </Button>

View File

@@ -6,7 +6,6 @@ import { HStack } from './core/Stacks';
import { RecentRequestsDropdown } from './RecentRequestsDropdown'; import { RecentRequestsDropdown } from './RecentRequestsDropdown';
import { RequestActionsDropdown } from './RequestActionsDropdown'; import { RequestActionsDropdown } from './RequestActionsDropdown';
import { SidebarActions } from './SidebarActions'; import { SidebarActions } from './SidebarActions';
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown'; import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
interface Props { interface Props {
@@ -24,7 +23,6 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
> >
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center"> <HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
<SidebarActions /> <SidebarActions />
<WorkspaceActionsDropdown className="pointer-events-auto" />
<EnvironmentActionsDropdown className="pointer-events-auto" /> <EnvironmentActionsDropdown className="pointer-events-auto" />
</HStack> </HStack>
<div className="pointer-events-none"> <div className="pointer-events-none">

View File

@@ -23,6 +23,7 @@ export type ButtonProps = HTMLAttributes<HTMLButtonElement> & {
forDropdown?: boolean; forDropdown?: boolean;
disabled?: boolean; disabled?: boolean;
title?: string; title?: string;
leftSlot?: ReactNode;
rightSlot?: ReactNode; rightSlot?: ReactNode;
}; };
@@ -37,6 +38,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
type = 'button', type = 'button',
justify = 'center', justify = 'center',
size = 'md', size = 'md',
leftSlot,
rightSlot, rightSlot,
disabled, disabled,
...props ...props
@@ -63,7 +65,11 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
return ( return (
<button ref={ref} type={type} className={classes} disabled={disabled} {...props}> <button ref={ref} type={type} className={classes} disabled={disabled} {...props}>
{isLoading && <Icon icon="update" size={size} className="animate-spin mr-1" />} {isLoading ? (
<Icon icon="update" size={size} className="animate-spin mr-1" />
) : leftSlot ? (
<div className="mr-1">{leftSlot}</div>
) : null}
{children} {children}
{rightSlot && <div className="ml-1">{rightSlot}</div>} {rightSlot && <div className="ml-1">{rightSlot}</div>}
{forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />} {forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />}

View File

@@ -35,6 +35,7 @@ export interface EditorProps {
onChange?: (value: string) => void; onChange?: (value: string) => void;
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void; onBlur?: () => void;
onSubmit?: () => void;
singleLine?: boolean; singleLine?: boolean;
wrapLines?: boolean; wrapLines?: boolean;
format?: (v: string) => string; format?: (v: string) => string;
@@ -56,6 +57,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
onChange, onChange,
onFocus, onFocus,
onBlur, onBlur,
onSubmit,
className, className,
singleLine, singleLine,
format, format,
@@ -77,6 +79,12 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
handleChange.current = onChange; handleChange.current = onChange;
}, [onChange]); }, [onChange]);
// Use ref so we can update the onChange handler without re-initializing the editor
const handleSubmit = useRef<EditorProps['onSubmit']>(onSubmit);
useEffect(() => {
handleSubmit.current = onSubmit;
}, [onSubmit]);
// Use ref so we can update the onChange handler without re-initializing the editor // Use ref so we can update the onChange handler without re-initializing the editor
const handleFocus = useRef<EditorProps['onFocus']>(onFocus); const handleFocus = useRef<EditorProps['onFocus']>(onFocus);
useEffect(() => { useEffect(() => {
@@ -113,6 +121,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
if (cm.current === null) return; if (cm.current === null) return;
const { view, languageCompartment } = cm.current; const { view, languageCompartment } = cm.current;
const ext = getLanguageExtension({ contentType, environment, useTemplating, autocomplete }); const ext = getLanguageExtension({ contentType, environment, useTemplating, autocomplete });
console.log("EXT", ext);
view.dispatch({ effects: languageCompartment.reconfigure(ext) }); view.dispatch({ effects: languageCompartment.reconfigure(ext) });
}, [contentType, autocomplete, useTemplating, environment]); }, [contentType, autocomplete, useTemplating, environment]);
@@ -147,6 +156,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
container, container,
readOnly, readOnly,
singleLine, singleLine,
onSubmit: handleSubmit,
onChange: handleChange, onChange: handleChange,
onFocus: handleFocus, onFocus: handleFocus,
onBlur: handleBlur, onBlur: handleBlur,
@@ -219,11 +229,13 @@ function getExtensions({
onChange, onChange,
onFocus, onFocus,
onBlur, onBlur,
onSubmit,
}: Pick<EditorProps, 'singleLine' | 'readOnly'> & { }: Pick<EditorProps, 'singleLine' | 'readOnly'> & {
container: HTMLDivElement | null; container: HTMLDivElement | null;
onChange: MutableRefObject<EditorProps['onChange']>; onChange: MutableRefObject<EditorProps['onChange']>;
onFocus: MutableRefObject<EditorProps['onFocus']>; onFocus: MutableRefObject<EditorProps['onFocus']>;
onBlur: MutableRefObject<EditorProps['onBlur']>; onBlur: MutableRefObject<EditorProps['onBlur']>;
onSubmit: MutableRefObject<EditorProps['onSubmit']>;
}) { }) {
// TODO: Ensure tooltips render inside the dialog if we are in one. // TODO: Ensure tooltips render inside the dialog if we are in one.
const parent = const parent =
@@ -249,10 +261,8 @@ function getExtensions({
}, },
keydown: (e) => { keydown: (e) => {
// Submit nearest form on enter if there is one // Submit nearest form on enter if there is one
if (e.key === 'Enter') { if (onSubmit != null && e.key === 'Enter') {
const el = e.currentTarget as HTMLElement; onSubmit.current?.();
const form = el.closest('form');
form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
} }
}, },
}), }),

View File

@@ -99,10 +99,10 @@ export function getLanguageExtension({
environment, environment,
autocomplete, autocomplete,
}: { environment: Environment | null } & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) { }: { environment: Environment | null } & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
if (contentType === 'application/graphql') { const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
if (justContentType === 'application/graphql') {
return graphql(); return graphql();
} }
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
const base = syntaxExtensions[justContentType] ?? text(); const base = syntaxExtensions[justContentType] ?? text();
if (!useTemplating) { if (!useTemplating) {
return base ? base : []; return base ? base : [];

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { EditorView } from 'codemirror'; import type { EditorView } from 'codemirror';
import type { HTMLAttributes, ReactNode } from 'react'; import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, useCallback, useMemo, useState } from 'react'; import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
import type { EditorProps } from './Editor'; import type { EditorProps } from './Editor';
import { Editor } from './Editor'; import { Editor } from './Editor';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
@@ -90,8 +90,17 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
[onChange], [onChange],
); );
const wrapperRef = useRef<HTMLDivElement>(null);
const handleSubmit = useCallback(() => {
const form = wrapperRef.current?.closest('form');
if (!isValid || form == null) return;
form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}, [isValid]);
return ( return (
<VStack className="w-full"> <VStack ref={wrapperRef} className="w-full">
<label <label
htmlFor={id} htmlFor={id}
className={classNames( className={classNames(
@@ -119,6 +128,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
ref={ref} ref={ref}
id={id} id={id}
singleLine singleLine
onSubmit={handleSubmit}
type={type === 'password' && !obscured ? 'text' : type} type={type === 'password' && !obscured ? 'text' : type}
defaultValue={defaultValue} defaultValue={defaultValue}
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}

View File

@@ -57,7 +57,7 @@ type BaseStackProps = HTMLAttributes<HTMLElement> & {
as?: ComponentType | 'ul' | 'form'; as?: ComponentType | 'ul' | 'form';
space?: keyof typeof gapClasses; space?: keyof typeof gapClasses;
alignItems?: 'start' | 'center'; alignItems?: 'start' | 'center';
justifyContent?: 'start' | 'center' | 'end'; justifyContent?: 'start' | 'center' | 'end' | 'between';
}; };
const BaseStack = forwardRef(function BaseStack( const BaseStack = forwardRef(function BaseStack(
@@ -77,6 +77,7 @@ const BaseStack = forwardRef(function BaseStack(
justifyContent === 'start' && 'justify-start', justifyContent === 'start' && 'justify-start',
justifyContent === 'center' && 'justify-center', justifyContent === 'center' && 'justify-center',
justifyContent === 'end' && 'justify-end', justifyContent === 'end' && 'justify-end',
justifyContent === 'between' && 'justify-between',
)} )}
{...props} {...props}
> >

View File

@@ -29,6 +29,7 @@ export function Prompt({ onHide, label, name, defaultValue, onResult }: PromptPr
<VStack space={6}> <VStack space={6}>
<Input <Input
hideLabel hideLabel
require
label={label} label={label}
name={name} name={name}
defaultValue={defaultValue} defaultValue={defaultValue}

View File

@@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveRequestId } from './useActiveRequestId'; import { useActiveRequestId } from './useActiveRequestId';
import type { Environment } from '../lib/models'; import type { Environment } from '../lib/models';
import { useCallback } from 'react';
export type RouteParamsWorkspace = { export type RouteParamsWorkspace = {
workspaceId: string; workspaceId: string;
@@ -39,38 +39,42 @@ export const routePaths = {
export function useAppRoutes() { export function useAppRoutes() {
const workspaceId = useActiveWorkspaceId(); const workspaceId = useActiveWorkspaceId();
const requestId = useActiveRequestId(); const requestId = useActiveRequestId();
const nav = useNavigate();
const navigate = useNavigate(); const navigate = useCallback(<T extends keyof typeof routePaths>(
return useMemo( path: T,
() => ({ ...params: Parameters<(typeof routePaths)[T]>
setEnvironment({ id: environmentId }: Environment) { ) => {
if (workspaceId == null) { // Not sure how to make TS work here, but it's good from the
this.navigate('workspaces'); // outside caller perspective.
} else if (requestId == null) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.navigate('workspace', { const resolvedPath = routePaths[path](...(params as any));
workspaceId: workspaceId, nav(resolvedPath);
environmentId: environmentId ?? null, }, [nav]);
});
} else { const setEnvironment = useCallback(
this.navigate('request', { ({ id: environmentId }: Environment) => {
workspaceId, if (workspaceId == null) {
environmentId: environmentId ?? null, navigate('workspaces');
requestId, } else if (requestId == null) {
}); navigate('workspace', {
} workspaceId: workspaceId,
}, environmentId: environmentId ?? null,
navigate<T extends keyof typeof routePaths>( });
path: T, } else {
...params: Parameters<(typeof routePaths)[T]> navigate('request', {
) { workspaceId,
// Not sure how to make TS work here, but it's good from the environmentId: environmentId ?? null,
// outside caller perspective. requestId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any });
const resolvedPath = routePaths[path](...(params as any)); }
navigate(resolvedPath); },
}, [navigate, workspaceId, requestId],
paths: routePaths,
}),
[navigate, requestId, workspaceId],
); );
return {
paths: routePaths,
navigate,
setEnvironment,
};
} }

View File

@@ -3,7 +3,10 @@ import { listen as tauriListen } from '@tauri-apps/api/event';
import type { DependencyList } from 'react'; import type { DependencyList } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
export function useTauriEvent<T>(event: string, fn: EventCallback<T>, deps: DependencyList = []) { /**
* React hook to listen to a Tauri event.
*/
export function useListenToTauriEvent<T>(event: string, fn: EventCallback<T>, deps: DependencyList = []) {
useEffect(() => { useEffect(() => {
let unMounted = false; let unMounted = false;
let unsubFn: (() => void) | undefined = undefined; let unsubFn: (() => void) | undefined = undefined;