mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-11 17:46:41 -05:00
Move workspace menu, better env mgmt, QoL
This commit is contained in:
@@ -340,11 +340,11 @@ async fn create_workspace(
|
||||
async fn create_environment(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
variables: Vec<models::EnvironmentVariable>,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Environment, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let variables = Vec::new();
|
||||
let created_environment = models::create_environment(workspace_id, name, variables, pool)
|
||||
.await
|
||||
.expect("Failed to create environment");
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import type { DialogProps } from './core/Dialog';
|
||||
import { Dialog } from './core/Dialog';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
|
||||
type DialogEntry = {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
@@ -9,6 +9,8 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -19,36 +21,53 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
}: Props) {
|
||||
const environments = useEnvironments();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
const showEnvironmentDialog = useCallback(() => {
|
||||
dialog.show({
|
||||
title: "Manage Environments",
|
||||
render: () => <EnvironmentEditDialog />,
|
||||
});
|
||||
}, [dialog]);
|
||||
|
||||
const items: DropdownItem[] = useMemo(
|
||||
() => [
|
||||
...environments.map(
|
||||
(e) => ({
|
||||
key: e.id,
|
||||
label: e.name,
|
||||
rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined,
|
||||
onSelect: async () => {
|
||||
routes.setEnvironment(e);
|
||||
},
|
||||
}),
|
||||
[activeEnvironment?.id],
|
||||
),
|
||||
{ type: 'separator', label: 'Environments' },
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: async () => {
|
||||
dialog.show({
|
||||
title: 'Environments',
|
||||
render: () => <EnvironmentEditDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
[activeEnvironment, dialog, environments, routes],
|
||||
() =>
|
||||
environments.length === 0
|
||||
? [
|
||||
{
|
||||
key: 'create',
|
||||
label: 'Create Environment',
|
||||
leftSlot: <Icon icon="plusCircle" />,
|
||||
onSelect: async () => {
|
||||
await createEnvironment.mutateAsync();
|
||||
showEnvironmentDialog();
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
...environments.map(
|
||||
(e) => ({
|
||||
key: e.id,
|
||||
label: e.name,
|
||||
rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined,
|
||||
onSelect: async () => {
|
||||
routes.setEnvironment(e);
|
||||
},
|
||||
}),
|
||||
[activeEnvironment?.id],
|
||||
),
|
||||
{ type: 'separator', label: 'Environments' },
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
},
|
||||
],
|
||||
[activeEnvironment, environments, routes, prompt, createEnvironment, showEnvironmentDialog],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import type { Environment } from '../lib/models';
|
||||
import { Button } from './core/Button';
|
||||
import classNames from 'classnames';
|
||||
@@ -8,22 +7,25 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { PairEditor } from './core/PairEditor';
|
||||
import type { PairEditorProps } from './core/PairEditor';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
|
||||
export const EnvironmentEditDialog = function () {
|
||||
const routes = useAppRoutes();
|
||||
const prompt = usePrompt();
|
||||
const environments = useEnvironments();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
|
||||
return (
|
||||
<div className="h-full grid gap-3 grid-cols-[auto_minmax(0,1fr)]">
|
||||
<aside className="relative h-full min-w-[200px] pr-3 border-r border-gray-200">
|
||||
<VStack space={0.5} className="relative h-full min-w-[200px] pr-3 border-r border-gray-100">
|
||||
{environments.map((e) => (
|
||||
<Button
|
||||
size="sm"
|
||||
size="xs"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
activeEnvironment?.id === e.id && 'bg-gray-100 text-gray-1000',
|
||||
@@ -41,39 +43,55 @@ export const EnvironmentEditDialog = function () {
|
||||
size="sm"
|
||||
className="mr-5 absolute bottom-0 left-0 right-0"
|
||||
color="gray"
|
||||
onClick={async () => {
|
||||
const name = await prompt({
|
||||
title: 'Environment Name',
|
||||
defaultValue: 'My Env',
|
||||
label: 'Name',
|
||||
name: 'environment',
|
||||
});
|
||||
createEnvironment.mutate({ name });
|
||||
}}
|
||||
onClick={() => createEnvironment.mutate()}
|
||||
>
|
||||
New Environment
|
||||
</Button>
|
||||
</aside>
|
||||
</VStack>
|
||||
{activeEnvironment != null && <EnvironmentEditor environment={activeEnvironment} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EnvironmentEditor = function ({ environment }: { environment: Environment }) {
|
||||
const environments = useEnvironments();
|
||||
const updateEnvironment = useUpdateEnvironment(environment.id);
|
||||
const deleteEnvironment = useDeleteEnvironment(environment);
|
||||
const handleChange = useCallback<PairEditorProps['onChange']>(
|
||||
(variables) => {
|
||||
updateEnvironment.mutate({ variables });
|
||||
},
|
||||
[updateEnvironment],
|
||||
);
|
||||
|
||||
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
||||
const otherVariableNames = environments.flatMap((e) => e.variables.map((v) => v.name));
|
||||
const variableNames = otherVariableNames.filter(
|
||||
(name) => !environment.variables.some((v) => v.name === name),
|
||||
);
|
||||
return { options: variableNames.map((name) => ({ label: name, type: 'constant' })) };
|
||||
}, [environments, environment.variables]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VStack space={2}>
|
||||
<HStack space={2} className="justify-between">
|
||||
<h1 className="text-xl">{environment.name}</h1>
|
||||
<IconButton
|
||||
icon="trash"
|
||||
title="Delete Environment"
|
||||
size="sm"
|
||||
className="!h-auto w-8"
|
||||
onClick={() => deleteEnvironment.mutate()}
|
||||
/>
|
||||
</HStack>
|
||||
<PairEditor
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
nameAutocompleteVariables={false}
|
||||
valueAutocompleteVariables={false}
|
||||
forceUpdateKey={environment.id}
|
||||
pairs={environment.variables}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ type Props = {
|
||||
export function HeaderEditor({ headers, onChange, forceUpdateKey }: Props) {
|
||||
return (
|
||||
<PairEditor
|
||||
valueAutocompleteVariables
|
||||
nameAutocompleteVariables
|
||||
pairs={headers}
|
||||
onChange={onChange}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
|
||||
@@ -16,13 +16,11 @@ import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { Icon } from './core/Icon';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import { DropMarker } from './DropMarker';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -34,7 +32,7 @@ enum ItemTypes {
|
||||
|
||||
export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
const { hidden } = useSidebarHidden();
|
||||
const createRequest = useCreateRequest({ navigateAfter: true });
|
||||
const createRequest = useCreateRequest();
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
@@ -156,15 +154,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
|
||||
return (
|
||||
<div aria-hidden={hidden} className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<HStack className="mt-1 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"
|
||||
onClick={() => createRequest.mutate({})}
|
||||
/>
|
||||
</HStack>
|
||||
<div
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { memo } from 'react';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
|
||||
export const SidebarActions = memo(function SidebarActions() {
|
||||
const createRequest = useCreateRequest();
|
||||
const { hidden, toggle } = useSidebarHidden();
|
||||
|
||||
if (!hidden) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon="plusCircle"
|
||||
title="Create Request"
|
||||
onClick={() => createRequest.mutate({})}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={toggle}
|
||||
className="pointer-events-auto"
|
||||
@@ -13,5 +24,6 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
title="Show sidebar"
|
||||
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classNames('url-bar', className)}>
|
||||
<Input
|
||||
autocompleteVariables
|
||||
ref={inputRef}
|
||||
size={isFocused ? 'auto' : 'sm'}
|
||||
hideLabel
|
||||
|
||||
@@ -29,7 +29,7 @@ const drag = { gridArea: 'drag' };
|
||||
|
||||
export default function Workspace() {
|
||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||
const { hide, hidden, toggle } = useSidebarHidden();
|
||||
const { hide, show, hidden, toggle } = useSidebarHidden();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const [floating, setFloating] = useState<boolean>(false);
|
||||
@@ -64,7 +64,14 @@ export default function Workspace() {
|
||||
moveState.current = {
|
||||
move: async (e: MouseEvent) => {
|
||||
e.preventDefault(); // Prevent text selection and things
|
||||
setWidth(startWidth + (e.clientX - mouseStartX));
|
||||
const newWidth = startWidth + (e.clientX - mouseStartX);
|
||||
if (newWidth < 100) {
|
||||
hide();
|
||||
resetWidth();
|
||||
} else {
|
||||
show();
|
||||
setWidth(newWidth);
|
||||
}
|
||||
},
|
||||
up: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -153,12 +153,9 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
return (
|
||||
<Dropdown items={items}>
|
||||
<Button
|
||||
forDropdown
|
||||
size="sm"
|
||||
className={classNames(className, 'text-gray-800 !px-2 truncate')}
|
||||
forDropdown
|
||||
leftSlot={
|
||||
<img src="https://yaak.app/logo.svg" alt="Workspace logo" className="w-4 h-4 mr-1" />
|
||||
}
|
||||
{...buttonProps}
|
||||
>
|
||||
{activeWorkspace?.name}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -23,6 +24,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
>
|
||||
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
|
||||
<SidebarActions />
|
||||
<WorkspaceActionsDropdown />
|
||||
<EnvironmentActionsDropdown className="pointer-events-auto" />
|
||||
</HStack>
|
||||
<div className="pointer-events-none">
|
||||
|
||||
@@ -4,12 +4,13 @@ import { Icon } from './Icon';
|
||||
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
title: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled }: Props) {
|
||||
export function Checkbox({ checked, onChange, className, disabled, title }: Props) {
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(!checked);
|
||||
}, [onChange, checked]);
|
||||
@@ -20,6 +21,7 @@ export function Checkbox({ checked, onChange, className, disabled }: Props) {
|
||||
aria-checked={checked ? 'true' : 'false'}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface DialogProps {
|
||||
children: ReactNode;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: ReactNode;
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
className?: string;
|
||||
size?: 'sm' | 'md' | 'full' | 'dynamic';
|
||||
@@ -63,9 +63,13 @@ export function Dialog({
|
||||
size === 'dynamic' && 'min-w-[30vw] max-w-[80vw]',
|
||||
)}
|
||||
>
|
||||
<Heading className="text-xl font-semibold w-full" id={titleId}>
|
||||
{title}
|
||||
</Heading>
|
||||
{title ? (
|
||||
<Heading className="text-xl font-semibold w-full" id={titleId}>
|
||||
{title}
|
||||
</Heading>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{description && <p id={descriptionId}>{description}</p>}
|
||||
<div className="h-full w-full">{children}</div>
|
||||
{/*Put close at the end so that it's the last thing to be tabbed to*/}
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface EditorProps {
|
||||
wrapLines?: boolean;
|
||||
format?: (v: string) => string;
|
||||
autocomplete?: GenericCompletionConfig;
|
||||
autocompleteVariables?: boolean;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
@@ -64,12 +65,14 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
singleLine,
|
||||
format,
|
||||
autocomplete,
|
||||
autocompleteVariables,
|
||||
actions,
|
||||
wrapLines,
|
||||
}: EditorProps,
|
||||
ref,
|
||||
) {
|
||||
const environment = useActiveEnvironment();
|
||||
const e = useActiveEnvironment();
|
||||
const environment = autocompleteVariables ? e : null;
|
||||
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
useImperativeHandle(ref, () => cm.current?.view);
|
||||
|
||||
@@ -37,7 +37,6 @@ import { text } from './text/extension';
|
||||
import { twig } from './twig/extension';
|
||||
import { url } from './url/extension';
|
||||
import type { Environment } from '../../../lib/models';
|
||||
import { EditorView } from 'codemirror';
|
||||
|
||||
export const myHighlightStyle = HighlightStyle.define([
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IconButton } from './IconButton';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
|
||||
export type InputProps = Omit<HTMLAttributes<HTMLInputElement>, 'onChange' | 'onFocus'> &
|
||||
Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete' | 'forceUpdateKey' | 'autoFocus' | 'autoSelect'> & {
|
||||
Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete' | 'forceUpdateKey' | 'autoFocus' | 'autoSelect' | 'autocompleteVariables'> & {
|
||||
name: string;
|
||||
type?: 'text' | 'password';
|
||||
label: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { InputProps } from './Input';
|
||||
import { Input } from './Input';
|
||||
import type { EditorView } from 'codemirror';
|
||||
|
||||
export type PairEditorProps = {
|
||||
pairs: Pair[];
|
||||
@@ -20,6 +21,8 @@ export type PairEditorProps = {
|
||||
valuePlaceholder?: string;
|
||||
nameAutocomplete?: GenericCompletionConfig;
|
||||
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
||||
nameAutocompleteVariables?: boolean;
|
||||
valueAutocompleteVariables?: boolean;
|
||||
nameValidate?: InputProps['validate'];
|
||||
valueValidate?: InputProps['validate'];
|
||||
};
|
||||
@@ -37,17 +40,20 @@ type PairContainer = {
|
||||
};
|
||||
|
||||
export const PairEditor = memo(function PairEditor({
|
||||
pairs: originalPairs,
|
||||
className,
|
||||
forceUpdateKey,
|
||||
nameAutocomplete,
|
||||
valueAutocomplete,
|
||||
nameAutocompleteVariables,
|
||||
namePlaceholder,
|
||||
valuePlaceholder,
|
||||
nameValidate,
|
||||
valueValidate,
|
||||
className,
|
||||
onChange,
|
||||
pairs: originalPairs,
|
||||
valueAutocomplete,
|
||||
valueAutocompleteVariables,
|
||||
valuePlaceholder,
|
||||
valueValidate,
|
||||
}: PairEditorProps) {
|
||||
const [forceFocusPairId, setForceFocusPairId] = useState<string | null>(null);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const [pairs, setPairs] = useState<PairContainer[]>(() => {
|
||||
// Remove empty headers on initial render
|
||||
@@ -105,6 +111,15 @@ export const PairEditor = memo(function PairEditor({
|
||||
[hoveredIndex, setPairsAndSave],
|
||||
);
|
||||
|
||||
const handleSubmitRow = useCallback(
|
||||
(pair: PairContainer) => {
|
||||
const index = pairs.findIndex((p) => p.id === pair.id);
|
||||
const id = pairs[index + 1]?.id ?? null;
|
||||
setForceFocusPairId(id);
|
||||
},
|
||||
[pairs],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(pair: PairContainer) =>
|
||||
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
|
||||
@@ -152,6 +167,9 @@ export const PairEditor = memo(function PairEditor({
|
||||
pairContainer={p}
|
||||
className="py-1"
|
||||
isLast={isLast}
|
||||
nameAutocompleteVariables={nameAutocompleteVariables}
|
||||
valueAutocompleteVariables={valueAutocompleteVariables}
|
||||
forceFocusPairId={forceFocusPairId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
valueAutocomplete={valueAutocomplete}
|
||||
@@ -159,6 +177,7 @@ export const PairEditor = memo(function PairEditor({
|
||||
valuePlaceholder={valuePlaceholder}
|
||||
nameValidate={nameValidate}
|
||||
valueValidate={valueValidate}
|
||||
onSubmit={handleSubmitRow}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onDelete={isLast ? undefined : handleDelete}
|
||||
@@ -179,16 +198,20 @@ enum ItemTypes {
|
||||
type FormRowProps = {
|
||||
className?: string;
|
||||
pairContainer: PairContainer;
|
||||
forceFocusPairId?: string | null;
|
||||
onMove: (id: string, side: 'above' | 'below') => void;
|
||||
onEnd: (id: string) => void;
|
||||
onChange: (pair: PairContainer) => void;
|
||||
onDelete?: (pair: PairContainer) => void;
|
||||
onFocus?: (pair: PairContainer) => void;
|
||||
onSubmit?: (pair: PairContainer) => void;
|
||||
isLast?: boolean;
|
||||
} & Pick<
|
||||
PairEditorProps,
|
||||
| 'nameAutocomplete'
|
||||
| 'valueAutocomplete'
|
||||
| 'nameAutocompleteVariables'
|
||||
| 'valueAutocompleteVariables'
|
||||
| 'namePlaceholder'
|
||||
| 'valuePlaceholder'
|
||||
| 'nameValidate'
|
||||
@@ -198,23 +221,34 @@ type FormRowProps = {
|
||||
|
||||
const FormRow = memo(function FormRow({
|
||||
className,
|
||||
pairContainer,
|
||||
forceFocusPairId,
|
||||
forceUpdateKey,
|
||||
isLast,
|
||||
nameAutocomplete,
|
||||
namePlaceholder,
|
||||
nameAutocompleteVariables,
|
||||
valueAutocompleteVariables,
|
||||
nameValidate,
|
||||
onChange,
|
||||
onDelete,
|
||||
onEnd,
|
||||
onFocus,
|
||||
onMove,
|
||||
onEnd,
|
||||
isLast,
|
||||
forceUpdateKey,
|
||||
nameAutocomplete,
|
||||
onSubmit,
|
||||
pairContainer,
|
||||
valueAutocomplete,
|
||||
namePlaceholder,
|
||||
valuePlaceholder,
|
||||
nameValidate,
|
||||
valueValidate,
|
||||
}: FormRowProps) {
|
||||
const { id } = pairContainer;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const nameInputRef = useRef<EditorView>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceFocusPairId === pairContainer.id) {
|
||||
nameInputRef.current?.focus();
|
||||
}
|
||||
}, [forceFocusPairId, pairContainer.id]);
|
||||
|
||||
const handleChangeEnabled = useMemo(
|
||||
() => (enabled: boolean) => onChange({ id, pair: { ...pairContainer.pair, enabled } }),
|
||||
@@ -237,7 +271,7 @@ const FormRow = memo(function FormRow({
|
||||
const [, connectDrop] = useDrop<PairContainer>(
|
||||
{
|
||||
accept: ItemTypes.ROW,
|
||||
hover: (item, monitor) => {
|
||||
hover: (_, monitor) => {
|
||||
if (!ref.current) return;
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
@@ -285,12 +319,18 @@ const FormRow = memo(function FormRow({
|
||||
<span className="w-3" />
|
||||
)}
|
||||
<Checkbox
|
||||
title={pairContainer.pair.enabled ? 'disable entry' : 'Enable item'}
|
||||
disabled={isLast}
|
||||
checked={isLast ? false : !!pairContainer.pair.enabled}
|
||||
className={classNames('mr-2', isLast && '!opacity-disabled')}
|
||||
onChange={handleChangeEnabled}
|
||||
/>
|
||||
<div
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSubmit?.(pairContainer);
|
||||
}}
|
||||
className={classNames(
|
||||
'grid items-center',
|
||||
'@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]',
|
||||
@@ -298,11 +338,12 @@ const FormRow = memo(function FormRow({
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
ref={nameInputRef}
|
||||
hideLabel
|
||||
useTemplating
|
||||
size="sm"
|
||||
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
|
||||
validate={nameValidate}
|
||||
useTemplating
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
containerClassName={classNames(isLast && 'border-dashed')}
|
||||
defaultValue={pairContainer.pair.name}
|
||||
@@ -312,9 +353,11 @@ const FormRow = memo(function FormRow({
|
||||
onFocus={handleFocus}
|
||||
placeholder={namePlaceholder ?? 'name'}
|
||||
autocomplete={nameAutocomplete}
|
||||
autocompleteVariables={nameAutocompleteVariables}
|
||||
/>
|
||||
<Input
|
||||
hideLabel
|
||||
useTemplating
|
||||
size="sm"
|
||||
containerClassName={classNames(isLast && 'border-dashed')}
|
||||
validate={valueValidate}
|
||||
@@ -325,10 +368,10 @@ const FormRow = memo(function FormRow({
|
||||
onChange={handleChangeValue}
|
||||
onFocus={handleFocus}
|
||||
placeholder={valuePlaceholder ?? 'value'}
|
||||
useTemplating
|
||||
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
|
||||
autocompleteVariables={valueAutocompleteVariables}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<IconButton
|
||||
aria-hidden={!onDelete}
|
||||
disabled={!onDelete}
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Environment } from '../lib/models';
|
||||
import { environmentsQueryKey } from './useEnvironments';
|
||||
import { environmentsQueryKey, useEnvironments } from './useEnvironments';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { usePrompt } from './usePrompt';
|
||||
import { useWorkspaces } from './useWorkspaces';
|
||||
|
||||
export function useCreateEnvironment() {
|
||||
const routes = useAppRoutes();
|
||||
const prompt = usePrompt();
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const queryClient = useQueryClient();
|
||||
const routes = useAppRoutes();
|
||||
const environments = useEnvironments();
|
||||
const workspaces = useWorkspaces();
|
||||
|
||||
return useMutation<Environment, unknown, Pick<Environment, 'name'>>({
|
||||
mutationFn: (patch) => {
|
||||
return invoke('create_environment', { ...patch, workspaceId });
|
||||
return useMutation<Environment, unknown, void>({
|
||||
mutationFn: async () => {
|
||||
const name = await prompt({
|
||||
name: 'name',
|
||||
title: 'Create Environment',
|
||||
description: 'Enter a name for the new environment',
|
||||
label: 'Name',
|
||||
defaultValue: 'My Environment',
|
||||
});
|
||||
const variables =
|
||||
environments.length === 0 && workspaces.length === 1
|
||||
? [{ name: 'first_variable', value: 'some reusable value' }]
|
||||
: [];
|
||||
return invoke('create_environment', { name, variables, workspaceId });
|
||||
},
|
||||
onSuccess: async (environment) => {
|
||||
if (workspaceId == null) return;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useAppRoutes } from './useAppRoutes';
|
||||
import { requestsQueryKey, useRequests } from './useRequests';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
|
||||
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
|
||||
export function useCreateRequest() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const routes = useAppRoutes();
|
||||
@@ -27,13 +27,11 @@ export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean })
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
(requests) => [...(requests ?? []), request],
|
||||
);
|
||||
if (navigateAfter) {
|
||||
routes.navigate('request', {
|
||||
workspaceId: request.workspaceId,
|
||||
requestId: request.id,
|
||||
environmentId: activeEnvironmentId ?? undefined,
|
||||
});
|
||||
}
|
||||
routes.navigate('request', {
|
||||
workspaceId: request.workspaceId,
|
||||
requestId: request.id,
|
||||
environmentId: activeEnvironmentId ?? undefined,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user