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