Merge pull request #256

* Update environment model to get ready for request/folder environments

* Folder environments in UI

* Folder environments working

* Tweaks and fixes

* Tweak environment encryption UX

* Tweak environment encryption UX

* Address comments

* Update fn name

* Add tsc back to lint rules

* Update src-web/components/EnvironmentEditor.tsx

* Merge remote-tracking branch 'origin/folder-environments' into folder…
This commit is contained in:
Gregory Schier
2025-09-21 07:54:26 -07:00
committed by GitHub
parent 46b049c72b
commit eb3d1c409b
85 changed files with 776 additions and 534 deletions

View File

@@ -4,5 +4,5 @@ import { useEnvironmentVariables } from './useEnvironmentVariables';
export function useActiveEnvironmentVariables() {
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
return useEnvironmentVariables(activeEnvironment?.id ?? null);
return useEnvironmentVariables(activeEnvironment?.id ?? null).map((v) => v.variable);
}

View File

@@ -7,7 +7,7 @@ export function useCopyHttpResponse(response: HttpResponse) {
return useFastMutation({
mutationKey: ['copy_http_response', response.id],
async mutationFn() {
const body = await getResponseBodyText(response);
const body = await getResponseBodyText({ responseId: response.id, filter: null });
copyToClipboard(body);
},
});

View File

@@ -0,0 +1,10 @@
import type { Environment } from '@yaakapp-internal/models';
import { useKeyValue } from './useKeyValue';
export function useEnvironmentValueVisibility(environment: Environment) {
return useKeyValue<boolean>({
namespace: 'global',
key: ['environmentValueVisibility', environment.workspaceId],
fallback: false,
});
}

View File

@@ -1,25 +1,52 @@
import type { EnvironmentVariable } from '@yaakapp-internal/models';
import { environmentsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models';
import { foldersAtom } from '@yaakapp-internal/models';
import { useMemo } from 'react';
import { jotaiStore } from '../lib/jotai';
import { useActiveRequest } from './useActiveRequest';
import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown';
import { useParentFolders } from './useParentFolders';
export function useEnvironmentVariables(environmentId: string | null) {
const { baseEnvironment } = useEnvironmentsBreakdown();
const activeEnvironment =
useAtomValue(environmentsAtom).find((e) => e.id === environmentId) ?? null;
const { baseEnvironment, folderEnvironments, subEnvironments } = useEnvironmentsBreakdown();
const activeEnvironment = subEnvironments.find((e) => e.id === environmentId) ?? null;
const activeRequest = useActiveRequest();
const parentFolders = useParentFolders(activeRequest);
return useMemo(() => {
const varMap: Record<string, EnvironmentVariable> = {};
const varMap: Record<string, WrappedEnvironmentVariable> = {};
const folderVariables = parentFolders.flatMap((f) =>
wrapVariables(folderEnvironments.find((fe) => fe.parentId === f.id) ?? null),
);
const allVariables = [
...(baseEnvironment?.variables ?? []),
...(activeEnvironment?.variables ?? []),
...folderVariables,
...wrapVariables(activeEnvironment),
...wrapVariables(baseEnvironment),
];
for (const v of allVariables) {
if (!v.enabled || !v.name) continue;
varMap[v.name] = v;
if (!v.variable.enabled || !v.variable.name || v.variable.name in varMap) {
continue;
}
varMap[v.variable.name] = v;
}
return Object.values(varMap);
}, [activeEnvironment, baseEnvironment]);
}, [activeEnvironment, baseEnvironment, folderEnvironments, parentFolders]);
}
export interface WrappedEnvironmentVariable {
variable: EnvironmentVariable;
environment: Environment;
source: string;
}
function wrapVariables(e: Environment | null): WrappedEnvironmentVariable[] {
if (e == null) return [];
const folders = jotaiStore.get(foldersAtom);
return e.variables.map((v) => {
const folder = e.parentModel === 'folder' ? folders.find((f) => f.id === e.parentId) : null;
const source = folder?.name ?? e.name;
return { variable: v, environment: e, source };
});
}

View File

@@ -5,12 +5,15 @@ import { useMemo } from 'react';
export function useEnvironmentsBreakdown() {
const allEnvironments = useAtomValue(environmentsAtom);
return useMemo(() => {
const baseEnvironments = allEnvironments.filter((e) => e.base) ?? [];
const subEnvironments = allEnvironments.filter((e) => !e.base) ?? [];
const baseEnvironments = allEnvironments.filter((e) => e.parentId == null) ?? [];
const subEnvironments =
allEnvironments.filter((e) => e.parentModel === 'environment' && e.parentId != null) ?? [];
const folderEnvironments =
allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? [];
const baseEnvironment = baseEnvironments[0] ?? null;
const otherBaseEnvironments =
baseEnvironments.filter((e) => e.id !== baseEnvironment?.id) ?? [];
return { allEnvironments, baseEnvironment, subEnvironments, otherBaseEnvironments };
return { allEnvironments, baseEnvironment, subEnvironments, folderEnvironments, otherBaseEnvironments };
}, [allEnvironments]);
}

View File

@@ -40,7 +40,7 @@ const hotkeys: Record<HotkeyAction, string[]> = {
'request_switcher.prev': ['Control+Tab'],
'request_switcher.toggle': ['CmdCtrl+p'],
'settings.show': ['CmdCtrl+,'],
'sidebar.delete_selected_item': ['Backspace'],
'sidebar.delete_selected_item': ['Delete'],
'sidebar.focus': ['CmdCtrl+b'],
'url_bar.focus': ['CmdCtrl+l'],
'workspace_settings.show': ['CmdCtrl+;'],
@@ -98,7 +98,7 @@ export function useHotKey(
// Don't add key if not holding modifier
const isValidKeymapKey =
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace';
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace' || e.key === 'Delete';
if (!isValidKeymapKey) {
return;
}

View File

@@ -18,7 +18,7 @@ import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useHttpAuthenticationConfig(
authName: string | null,
values: Record<string, JsonPrimitive>,
requestId: string,
request: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
) {
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const environmentId = useAtomValue(activeEnvironmentIdAtom);
@@ -37,7 +37,7 @@ export function useHttpAuthenticationConfig(
return useQuery({
queryKey: [
'http_authentication_config',
requestId,
request,
authName,
values,
responseKey,
@@ -53,8 +53,7 @@ export function useHttpAuthenticationConfig(
{
authName,
values,
requestId,
workspaceId,
request,
environmentId,
},
);
@@ -63,17 +62,16 @@ export function useHttpAuthenticationConfig(
...config,
actions: config.actions?.map((a, i) => ({
...a,
call: async ({
id: modelId,
}: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace) => {
call: async (
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
) => {
await invokeCmd('cmd_call_http_authentication_action', {
pluginRefId: config.pluginRefId,
actionIndex: i,
authName,
values,
modelId,
model,
environmentId,
workspaceId,
});
// Ensure the config is refreshed after the action is done

View File

@@ -66,7 +66,7 @@ export function useIntrospectGraphQL(
return setError(response.error);
}
const bodyText = await getResponseBodyText(response);
const bodyText = await getResponseBodyText({ responseId: response.id, filter: null });
if (response.status < 200 || response.status >= 300) {
return setError(
`Request failed with status ${response.status}.\nThe response text is:\n\n${bodyText}`,

View File

@@ -1,7 +1,7 @@
import {useAtomValue} from "jotai/index";
import {activeWorkspaceMetaAtom} from "./useActiveWorkspace";
import { useAtomValue } from 'jotai';
import { activeWorkspaceMetaAtom } from './useActiveWorkspace';
export function useIsEncryptionEnabled() {
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
return workspaceMeta?.encryptionKey != null;
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
return workspaceMeta?.encryptionKey != null;
}

View File

@@ -0,0 +1,24 @@
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { foldersAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
export function useParentFolders(m: Folder | HttpRequest | GrpcRequest | WebsocketRequest | null) {
const folders = useAtomValue(foldersAtom);
return useMemo(() => getParentFolders(folders, m), [folders, m]);
}
function getParentFolders(
folders: Folder[],
currentModel: Folder | HttpRequest | GrpcRequest | WebsocketRequest | null,
): Folder[] {
if (currentModel == null) return [];
const folder = currentModel.folderId ? folders.find((f) => f.id === currentModel.folderId) : null;
if (folder == null) {
return [];
}
return [folder, ...getParentFolders(folders, folder)];
}

View File

@@ -1,15 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import type { HttpResponse } from '@yaakapp-internal/models';
import { getResponseBodyText } from '../lib/responseBody';
export function useResponseBodyText({
responseId,
response,
filter,
}: {
responseId: string;
response: HttpResponse;
filter: string | null;
}) {
return useQuery({
queryKey: ['response_body_text', responseId, filter ?? ''],
queryFn: () => getResponseBodyText({ responseId, filter }),
queryKey: [
'response_body_text',
response.id,
response.updatedAt,
response.contentLength,
filter ?? '',
],
queryFn: () => getResponseBodyText({ responseId: response.id, filter }),
});
}