mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-22 07:08:16 -04:00
response routing (#6214)
* pass 1 * refactoring notes * simplify select children * remove switcher * add responses * filter by env * patch settings * patch settings * remove selector * history dereduxed * move folder settings to conditional render * fix types * fix types * fix types * fix types * fix lint * migrate group settings to remix * fix use effects * conditional render of export modal * more sidebar refactoring * unpack sidebar children in export list * unpick sidebar children * clean up * remove console log * mostly working connect * clean up * conditionally render cookie modals * cond render env modal * cond render sync delete * fix * fix * flatten syncitems * remove helper * clean * fix grpc initial re-render * redirect to last active request * optimize * fix lint * improve typing * add settings hotkey * env modal hotkey * remove active response null setting * fix shakey test * move delete response to action * fix type * fix windows flake * cond render sync branches * fix loading bug * add spinner * delete request * fix stream with temporary hack
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import { stat } from 'node:fs/promises';
|
||||
|
||||
import NeDB from 'nedb';
|
||||
import path from 'path';
|
||||
|
||||
@@ -8,7 +9,9 @@ import type { BaseModel } from '../models/types';
|
||||
|
||||
const neDbAdapter: DbAdapter = async (dir, filterTypes) => {
|
||||
// Confirm if db files exist
|
||||
if (!fs.existsSync(path.join(dir, 'insomnia.Workspace.db'))) {
|
||||
try {
|
||||
await stat(path.join(dir, 'insomnia.Workspace.db'));
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ test('can send GraphQL requests after editing and prettifying query', async ({ a
|
||||
await page.getByRole('button', { name: 'GraphQL request' }).click();
|
||||
|
||||
// Edit and prettify query
|
||||
await page.locator('pre[role="presentation"]:has-text("hello,")').click();
|
||||
await page.locator('pre[role="presentation"]:has-text("bearer")').click();
|
||||
await page.locator('.app').press('Enter');
|
||||
await page.locator('text=Prettify GraphQL').click();
|
||||
await page.click('[data-testid="request-pane"] >> text=Send');
|
||||
|
||||
@@ -317,7 +317,6 @@ export async function getRenderContext(
|
||||
|
||||
const project = ancestors.find(isProject);
|
||||
const workspace = ancestors.find(isWorkspace);
|
||||
console.log('getRenderContext', { request, environmentId, ancestors, purpose, extraInfo, project, workspace });
|
||||
if (!workspace) {
|
||||
throw new Error('Failed to render. Could not find workspace');
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { StatusCandidate } from '../../sync/types';
|
||||
import { BaseModel, canSync } from '..';
|
||||
|
||||
const toStatusCandidate = (doc: BaseModel): StatusCandidate => ({
|
||||
key: doc._id,
|
||||
name: doc.name || '',
|
||||
document: doc,
|
||||
});
|
||||
|
||||
export const getStatusCandidates = (docs: BaseModel[]) => docs.filter(canSync).map(toStatusCandidate);
|
||||
@@ -53,6 +53,10 @@ export function getById(id: string) {
|
||||
return db.get<RequestVersion>(type, id);
|
||||
}
|
||||
|
||||
export function findByParentId(parentId: string) {
|
||||
return db.find<RequestVersion>(type, { parentId });
|
||||
}
|
||||
|
||||
export async function create(request: Request | WebSocketRequest | GrpcRequest) {
|
||||
if (!isRequest(request) && !isWebSocketRequest(request)) {
|
||||
throw new Error(`New ${type} was not given a valid ${request.type} instance`);
|
||||
|
||||
@@ -109,6 +109,10 @@ export function getById(id: string) {
|
||||
return db.get<Response>(type, id);
|
||||
}
|
||||
|
||||
export function findByParentId(parentId: string) {
|
||||
return db.find<Response>(type, { parentId: parentId });
|
||||
}
|
||||
|
||||
export async function all() {
|
||||
return db.all<Response>(type);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ export function getById(id: string) {
|
||||
return db.get<WebSocketResponse>(type, id);
|
||||
}
|
||||
|
||||
export function findByParentId(parentId: string) {
|
||||
return db.find<WebSocketResponse>(type, { parentId: parentId });
|
||||
}
|
||||
|
||||
export async function all() {
|
||||
return db.all<WebSocketResponse>(type);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { database } from '../../common/database';
|
||||
import * as models from '../../models';
|
||||
import { getStatusCandidates } from '../../models/helpers/get-status-candidates';
|
||||
import { BaseModel, canSync } from '../../models';
|
||||
import { Project } from '../../models/project';
|
||||
import { Workspace } from '../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../models/workspace-meta';
|
||||
import { StatusCandidate } from '../types';
|
||||
import { VCS } from './vcs';
|
||||
|
||||
const blankStage = {};
|
||||
@@ -13,7 +14,11 @@ export const initializeLocalBackendProjectAndMarkForSync = async ({ vcs, workspa
|
||||
await vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name);
|
||||
|
||||
// Everything unstaged
|
||||
const candidates = getStatusCandidates(await database.withDescendants(workspace));
|
||||
const candidates = (await database.withDescendants(workspace)).filter(canSync).map((doc: BaseModel): StatusCandidate => ({
|
||||
key: doc._id,
|
||||
name: doc.name || '',
|
||||
document: doc,
|
||||
}));
|
||||
const status = await vcs.status(candidates, blankStage);
|
||||
|
||||
// Stage everything
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Cookie as ToughCookie } from 'tough-cookie';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -6,7 +6,6 @@ import { cookieToString } from '../../common/cookies';
|
||||
import { Cookie } from '../../models/cookie-jar';
|
||||
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from './base/dropdown';
|
||||
import { PromptButton } from './base/prompt-button';
|
||||
import { showModal } from './modals';
|
||||
import { CookieModifyModal } from './modals/cookie-modify-modal';
|
||||
import { RenderedText } from './rendered-text';
|
||||
|
||||
@@ -27,6 +26,7 @@ const CookieRow: FC<{
|
||||
index: number;
|
||||
deleteCookie: (cookie: Cookie) => void;
|
||||
}> = ({ cookie, index, deleteCookie }) => {
|
||||
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
|
||||
const c = ToughCookie.fromJSON(cookie);
|
||||
const cookieString = c ? cookieToString(c) : '';
|
||||
return <tr className="selectable" key={index}>
|
||||
@@ -39,7 +39,7 @@ const CookieRow: FC<{
|
||||
<td onClick={() => { }} className="text-right no-wrap">
|
||||
<button
|
||||
className="btn btn--super-compact btn--outlined"
|
||||
onClick={() => showModal(CookieModifyModal, { cookie })}
|
||||
onClick={() => setIsCookieModalOpen(true)}
|
||||
title="Edit cookie properties"
|
||||
>
|
||||
Edit
|
||||
@@ -52,6 +52,12 @@ const CookieRow: FC<{
|
||||
>
|
||||
<i className="fa fa-trash-o" />
|
||||
</PromptButton>
|
||||
{isCookieModalOpen && (
|
||||
<CookieModifyModal
|
||||
cookie={cookie}
|
||||
onHide={() => setIsCookieModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getAuthTypeName,
|
||||
HAWK_ALGORITHM_SHA256,
|
||||
} from '../../../common/constants';
|
||||
import { Request, RequestAuthentication } from '../../../models/request';
|
||||
import { RequestAuthentication } from '../../../models/request';
|
||||
import { SIGNATURE_METHOD_HMAC_SHA1 } from '../../../network/o-auth-1/constants';
|
||||
import { GRANT_TYPE_AUTHORIZATION_CODE } from '../../../network/o-auth-2/constants';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
@@ -129,7 +129,7 @@ interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
export const AuthDropdown: FC<Props> = ({ authTypes = defaultTypes, disabled = false }) => {
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const { requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const patchRequest = useRequestPatcher();
|
||||
const onClick = useCallback(async (type: AuthType) => {
|
||||
|
||||
@@ -29,7 +29,7 @@ import { showModal } from '../modals/index';
|
||||
const EMPTY_MIME_TYPE = null;
|
||||
|
||||
export const ContentTypeDropdown: FC = () => {
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const patchRequest = useRequestPatcher();
|
||||
const { requestId } = useParams() as { requestId: string };
|
||||
const handleChangeMimeType = async (mimeType: string | null) => {
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React, { FC, useRef, useState } from 'react';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import type { Environment } from '../../../models/environment';
|
||||
import { type Environment } from '../../../models/environment';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { useDocBodyKeyboardShortcuts } from '../keydown-binder';
|
||||
import { showModal } from '../modals/index';
|
||||
import { WorkspaceEnvironmentsEditModal } from '../modals/workspace-environments-edit-modal';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
interface Props {
|
||||
activeEnvironment?: Environment | null;
|
||||
workspaceId: string;
|
||||
setEnvironmentModalOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const EnvironmentsDropdown: FC<Props> = () => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string}>();
|
||||
export const EnvironmentsDropdown: FC<Props> = ({ setEnvironmentModalOpen }) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const {
|
||||
baseEnvironment,
|
||||
activeEnvironment,
|
||||
@@ -89,10 +88,10 @@ export const EnvironmentsDropdown: FC<Props> = () => {
|
||||
setActiveEnvironmentFetcher.submit({
|
||||
environmentId: environment._id,
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
|
||||
});
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
@@ -107,22 +106,17 @@ export const EnvironmentsDropdown: FC<Props> = () => {
|
||||
setActiveEnvironmentFetcher.submit({
|
||||
environmentId: baseEnvironment._id,
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
|
||||
});
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownSection title="General">
|
||||
<DropdownItem>
|
||||
<ItemContent
|
||||
icon="wrench"
|
||||
label="Manage Environments"
|
||||
hint={hotKeyRegistry.environment_showEditor}
|
||||
onClick={() => showModal(WorkspaceEnvironmentsEditModal)}
|
||||
/>
|
||||
<ItemContent icon="wrench" label="Manage Environments" hint={hotKeyRegistry.environment_showEditor} onClick={() => setEnvironmentModalOpen(true)} />
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
|
||||
@@ -167,7 +167,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository, isInsomni
|
||||
gitPushFetcher.state === 'loading' ||
|
||||
gitPullFetcher.state === 'loading';
|
||||
|
||||
const isSynced = Boolean(gitRepository?.uri && !isLoading && gitRepoDataFetcher.data && !('errors' in gitRepoDataFetcher.data));
|
||||
const isSynced = Boolean(gitRepository?.uri && gitRepoDataFetcher.data && !('errors' in gitRepoDataFetcher.data));
|
||||
|
||||
const { branches, branch: currentBranch } =
|
||||
gitRepoDataFetcher.data && 'branches' in gitRepoDataFetcher.data
|
||||
@@ -263,6 +263,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository, isInsomni
|
||||
justifyContent: 'flex-start !important',
|
||||
height: 'var(--line-height-sm)',
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -289,7 +290,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository, isInsomni
|
||||
opacity: status?.localChanges ? 1 : 0.5,
|
||||
color: status?.localChanges ? 'var(--color-notice)' : 'var(--color-hl)',
|
||||
}}
|
||||
><i className="fa fa-cube space-left" /></span>
|
||||
><i className={`fa fa-${isLoading ? 'refresh fa-spin' : 'cube'} space-left`} /></span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import fs from 'fs';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { getPreviewModeName, PREVIEW_MODE_SOURCE, PREVIEW_MODES, PreviewMode } from '../../../common/constants';
|
||||
import { getPreviewModeName, PREVIEW_MODE_SOURCE, PREVIEW_MODES } from '../../../common/constants';
|
||||
import { exportHarCurrentRequest } from '../../../common/har';
|
||||
import * as models from '../../../models';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import { RequestMeta } from '../../../models/request-meta';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { isResponse } from '../../../models/response';
|
||||
import { useRequestMetaPatcher } from '../../hooks/use-request';
|
||||
import { selectActiveResponse } from '../../redux/selectors';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
|
||||
@@ -23,24 +20,20 @@ export const PreviewModeDropdown: FC<Props> = ({
|
||||
download,
|
||||
copyToClipboard,
|
||||
}) => {
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
|
||||
const { activeRequest, activeRequestMeta, activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const previewMode = activeRequestMeta.previewMode || PREVIEW_MODE_SOURCE;
|
||||
const response = useSelector(selectActiveResponse);
|
||||
const patchRequestMeta = useRequestMetaPatcher();
|
||||
const handleClick = async (previewMode: PreviewMode) => {
|
||||
patchRequestMeta(activeRequest._id, { previewMode });
|
||||
};
|
||||
const handleDownloadPrettify = useCallback(() => download(true), [download]);
|
||||
|
||||
const handleDownloadNormal = useCallback(() => download(false), [download]);
|
||||
|
||||
const exportAsHAR = useCallback(async () => {
|
||||
if (!response || !activeRequest || !isRequest(activeRequest) || !isResponse(response)) {
|
||||
if (!activeResponse || !activeRequest || !isRequest(activeRequest) || !isResponse(activeResponse)) {
|
||||
console.warn('Nothing to download');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await exportHarCurrentRequest(activeRequest, response);
|
||||
const data = await exportHarCurrentRequest(activeRequest, activeResponse);
|
||||
const har = JSON.stringify(data, null, '\t');
|
||||
|
||||
const { filePath } = await window.dialog.showSaveDialog({
|
||||
@@ -57,15 +50,15 @@ export const PreviewModeDropdown: FC<Props> = ({
|
||||
console.warn('Failed to export har', err);
|
||||
});
|
||||
to.end(har);
|
||||
}, [activeRequest, response]);
|
||||
}, [activeRequest, activeResponse]);
|
||||
|
||||
const exportDebugFile = useCallback(async () => {
|
||||
if (!response || !activeRequest || !isResponse(response)) {
|
||||
if (!activeResponse || !activeRequest || !isResponse(activeResponse)) {
|
||||
console.warn('Nothing to download');
|
||||
return;
|
||||
}
|
||||
|
||||
const timeline = models.response.getTimeline(response);
|
||||
const timeline = models.response.getTimeline(activeResponse);
|
||||
const headers = timeline
|
||||
.filter(v => v.name === 'HeaderIn')
|
||||
.map(v => v.value)
|
||||
@@ -80,7 +73,7 @@ export const PreviewModeDropdown: FC<Props> = ({
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
const readStream = models.response.getBodyStream(response);
|
||||
const readStream = models.response.getBodyStream(activeResponse);
|
||||
|
||||
if (readStream && filePath && typeof readStream !== 'string') {
|
||||
const to = fs.createWriteStream(filePath);
|
||||
@@ -90,8 +83,8 @@ export const PreviewModeDropdown: FC<Props> = ({
|
||||
console.warn('Failed to save full response', err);
|
||||
});
|
||||
}
|
||||
}, [activeRequest, response]);
|
||||
const shouldPrettifyOption = response.contentType.includes('json');
|
||||
}, [activeRequest, activeResponse]);
|
||||
const shouldPrettifyOption = activeResponse?.contentType.includes('json');
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@@ -115,7 +108,7 @@ export const PreviewModeDropdown: FC<Props> = ({
|
||||
<ItemContent
|
||||
icon={previewMode === mode ? 'check' : 'empty'}
|
||||
label={getPreviewModeName(mode, true)}
|
||||
onClick={() => handleClick(mode)}
|
||||
onClick={() => patchRequestMeta(activeRequest._id, { previewMode: mode })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { differenceInHours, differenceInMinutes, isThisWeek, isToday } from 'date-fns';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import { useFetcher, useRouteLoaderData } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { decompressObject } from '../../../common/misc';
|
||||
@@ -11,7 +10,7 @@ import { Response } from '../../../models/response';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { isWebSocketResponse, WebSocketResponse } from '../../../models/websocket-response';
|
||||
import { useRequestMetaPatcher } from '../../hooks/use-request';
|
||||
import { selectActiveRequestResponses, selectRequestVersions } from '../../redux/selectors';
|
||||
import { RequestLoaderData, WebSocketRequestLoaderData } from '../../routes/request';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { useDocBodyKeyboardShortcuts } from '../keydown-binder';
|
||||
@@ -20,25 +19,22 @@ import { StatusTag } from '../tags/status-tag';
|
||||
import { TimeTag } from '../tags/time-tag';
|
||||
import { URLTag } from '../tags/url-tag';
|
||||
import { TimeFromNow } from '../time-from-now';
|
||||
interface Props<GenericResponse extends Response | WebSocketResponse> {
|
||||
activeResponse: GenericResponse;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSocketResponse>({
|
||||
export const ResponseHistoryDropdown = ({
|
||||
activeResponse,
|
||||
className,
|
||||
}: Props<GenericResponse>) => {
|
||||
}: { activeResponse: Response | WebSocketResponse }) => {
|
||||
const { requestId } = useParams() as { requestId: string };
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
const patchRequestMeta = useRequestMetaPatcher();
|
||||
const {
|
||||
activeEnvironment,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const responses = useSelector(selectActiveRequestResponses) as GenericResponse[];
|
||||
const requestVersions = useSelector(selectRequestVersions);
|
||||
const {
|
||||
responses,
|
||||
requestVersions,
|
||||
} = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData;
|
||||
const now = new Date();
|
||||
const categories: Record<string, GenericResponse[]> = {
|
||||
const categories: Record<string, (Response | WebSocketResponse)[]> = {
|
||||
minutes: [],
|
||||
hours: [],
|
||||
today: [],
|
||||
@@ -46,6 +42,9 @@ export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSo
|
||||
other: [],
|
||||
};
|
||||
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const fetcher = useFetcher();
|
||||
|
||||
const handleSetActiveResponse = useCallback(async (requestId: string, activeResponse: Response | WebSocketResponse) => {
|
||||
if (isWebSocketResponse(activeResponse)) {
|
||||
window.main.webSocket.close({ requestId });
|
||||
@@ -61,63 +60,40 @@ export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSo
|
||||
const handleDeleteResponses = useCallback(async () => {
|
||||
if (isWebSocketResponse(activeResponse)) {
|
||||
window.main.webSocket.closeAll();
|
||||
await models.webSocketResponse.removeForRequest(requestId, activeEnvironment._id);
|
||||
} else {
|
||||
await models.response.removeForRequest(requestId, activeEnvironment._id);
|
||||
}
|
||||
await patchRequestMeta(requestId, { activeResponseId: null });
|
||||
}, [activeEnvironment._id, activeResponse, requestId, patchRequestMeta]);
|
||||
fetcher.submit({}, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/response/delete-all`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}, [activeResponse, fetcher, organizationId, projectId, requestId, workspaceId]);
|
||||
|
||||
const handleDeleteResponse = useCallback(async () => {
|
||||
let response: Response | WebSocketResponse | null = null;
|
||||
if (activeResponse) {
|
||||
if (isWebSocketResponse(activeResponse)) {
|
||||
window.main.webSocket.close({ requestId });
|
||||
await models.webSocketResponse.remove(activeResponse);
|
||||
const environmentId = activeEnvironment?._id || null;
|
||||
response = await models.webSocketResponse.getLatestForRequest(requestId, environmentId);
|
||||
} else {
|
||||
await models.response.remove(activeResponse);
|
||||
const environmentId = activeEnvironment?._id || null;
|
||||
response = await models.response.getLatestForRequest(requestId, environmentId);
|
||||
}
|
||||
|
||||
if (response?.requestVersionId) {
|
||||
// Deleting a response restores latest request body
|
||||
await models.requestVersion.restore(response.requestVersionId);
|
||||
}
|
||||
|
||||
await patchRequestMeta(requestId, { activeResponseId: response?._id || null });
|
||||
}
|
||||
}, [activeEnvironment?._id, activeResponse, requestId, patchRequestMeta]);
|
||||
fetcher.submit({ responseId: activeResponse._id }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/response/delete`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}, [activeResponse, fetcher, requestId, organizationId, projectId, workspaceId]);
|
||||
|
||||
responses.forEach(response => {
|
||||
responses.forEach((response: Response | WebSocketResponse) => {
|
||||
const responseTime = new Date(response.created);
|
||||
|
||||
if (differenceInMinutes(now, responseTime) < 5) {
|
||||
categories.minutes.push(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (differenceInHours(now, responseTime) < 2) {
|
||||
categories.hours.push(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isToday(responseTime)) {
|
||||
categories.today.push(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isThisWeek(responseTime)) {
|
||||
categories.week.push(response);
|
||||
return;
|
||||
}
|
||||
|
||||
categories.other.push(response);
|
||||
const match = Object.entries({
|
||||
'minutes': differenceInMinutes(now, responseTime) < 5,
|
||||
'hours': differenceInHours(now, responseTime) < 2,
|
||||
'today': isToday(responseTime),
|
||||
'week': isThisWeek(responseTime),
|
||||
'other': true,
|
||||
}).find(([, value]) => value === true)?.[0] || 'other';
|
||||
categories[match].push(response);
|
||||
});
|
||||
|
||||
const renderResponseRow = (response: GenericResponse) => {
|
||||
const renderResponseRow = (response: Response | WebSocketResponse) => {
|
||||
const activeResponseId = activeResponse ? activeResponse._id : 'n/a';
|
||||
const active = response._id === activeResponseId;
|
||||
const requestVersion = requestVersions.find(({ _id }) => _id === response.requestVersionId);
|
||||
@@ -180,7 +156,7 @@ export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSo
|
||||
aria-label="Response history dropdown"
|
||||
key={activeResponse ? activeResponse._id : 'n/a'}
|
||||
closeOnSelect={false}
|
||||
className={className}
|
||||
className="tall pane__header__right"
|
||||
triggerButton={
|
||||
<DropdownButton className="btn btn--super-compact tall" title="Response history">
|
||||
{activeResponse && <TimeFromNow timestamp={activeResponse.created} titleCase />}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { BackendProjectWithTeam } from '../../../sync/vcs/normalize-backend-proj
|
||||
import { pullBackendProject } from '../../../sync/vcs/pull-backend-project';
|
||||
import { interceptAccessError } from '../../../sync/vcs/util';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { selectRemoteProjects, selectSyncItems } from '../../redux/selectors';
|
||||
import { selectSyncItems } from '../../redux/selectors';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { Link } from '../base/link';
|
||||
@@ -81,11 +81,13 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
});
|
||||
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const remoteProjects = useSelector(selectRemoteProjects);
|
||||
const syncItems = useSelector(selectSyncItems);
|
||||
const {
|
||||
activeWorkspaceMeta,
|
||||
projects,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const remoteProjects = projects.filter(isRemoteProject);
|
||||
|
||||
const refetchRemoteBranch = useCallback(async () => {
|
||||
if (session.isLoggedIn()) {
|
||||
try {
|
||||
@@ -130,7 +132,10 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
}, REFRESH_PERIOD);
|
||||
|
||||
const [isGitRepoSettingsModalOpen, setIsGitRepoSettingsModalOpen] = useState(false);
|
||||
|
||||
const [isSyncDeleteModalOpen, setIsSyncDeleteModalOpen] = useState(false);
|
||||
// const [isSyncHistoryModalOpen, setIsSyncHistoryModalOpen] = useState(false);
|
||||
// const [isSyncStagingModalOpen, setIsSyncStagingModalOpen] = useState(false);
|
||||
const [isSyncBranchesModalOpen, setIsSyncBranchesModalOpen] = useState(false);
|
||||
useMount(async () => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
@@ -577,7 +582,7 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
<ItemContent
|
||||
icon="code-fork"
|
||||
label="Branches"
|
||||
onClick={() => showModal(SyncBranchesModal, { onHide: refreshVCSAndRefetchRemote })}
|
||||
onClick={() => setIsSyncBranchesModalOpen(true)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
|
||||
@@ -586,7 +591,7 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
icon="remove"
|
||||
isDisabled={historyCount === 0}
|
||||
label={<>Delete {strings.collection.singular}</>}
|
||||
onClick={() => showModal(SyncDeleteModal, { onHide: refreshVCSAndRefetchRemote })}
|
||||
onClick={() => setIsSyncDeleteModalOpen(true)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
@@ -675,6 +680,24 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
onHide={() => setIsGitRepoSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isSyncDeleteModalOpen && (
|
||||
<SyncDeleteModal
|
||||
vcs={vcs}
|
||||
onHide={() => {
|
||||
refreshVCSAndRefetchRemote();
|
||||
setIsSyncDeleteModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isSyncBranchesModalOpen && (
|
||||
<SyncBranchesModal
|
||||
vcs={vcs}
|
||||
onHide={() => {
|
||||
refreshVCSAndRefetchRemote();
|
||||
setIsSyncBranchesModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,6 +88,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
const fetcher = useFetcher();
|
||||
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
const {
|
||||
organizationId,
|
||||
@@ -151,7 +152,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
<ItemContent
|
||||
label="Export"
|
||||
icon="file-export"
|
||||
onClick={() => showModal(ExportRequestsModal)}
|
||||
onClick={() => setIsExportModalOpen(true)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Settings'>
|
||||
@@ -214,6 +215,11 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
defaultWorkspaceId={workspace._id}
|
||||
/>
|
||||
)}
|
||||
{isExportModalOpen && (
|
||||
<ExportRequestsModal
|
||||
onHide={() => setIsExportModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isSettingsModalOpen && (
|
||||
<WorkspaceSettingsModal
|
||||
workspace={workspace}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useAIContext } from '../../context/app/ai-context';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { InsomniaAI } from '../insomnia-ai-icon';
|
||||
import { showError, showModal, showPrompt } from '../modals';
|
||||
import { showError, showPrompt } from '../modals';
|
||||
import { ExportRequestsModal } from '../modals/export-requests-modal';
|
||||
import { configGenerators, showGenerateConfigModal } from '../modals/generate-config-modal';
|
||||
import { ImportModal } from '../modals/import-modal';
|
||||
@@ -40,6 +40,7 @@ export const WorkspaceDropdown: FC = () => {
|
||||
const activeWorkspaceName = activeWorkspace.name;
|
||||
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
const workspaceName = activeWorkspace.name;
|
||||
const projectName = activeProject.name ?? getProductName();
|
||||
@@ -166,13 +167,13 @@ export const WorkspaceDropdown: FC = () => {
|
||||
/>
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem aria-label='Export'>
|
||||
<ItemContent
|
||||
icon="file-export"
|
||||
label="Export"
|
||||
onClick={() => showModal(ExportRequestsModal)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Export'>
|
||||
<ItemContent
|
||||
icon="file-export"
|
||||
label="Export"
|
||||
onClick={() => setIsExportModalOpen(true)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem aria-label="Settings">
|
||||
<ItemContent
|
||||
@@ -272,6 +273,11 @@ export const WorkspaceDropdown: FC = () => {
|
||||
defaultWorkspaceId={workspaceId}
|
||||
/>
|
||||
)}
|
||||
{isExportModalOpen && (
|
||||
<ExportRequestsModal
|
||||
onHide={() => setIsExportModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isSettingsModalOpen && (
|
||||
<WorkspaceSettingsModal
|
||||
workspace={activeWorkspace}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
AUTH_OAUTH_1,
|
||||
AUTH_OAUTH_2,
|
||||
} from '../../../../common/constants';
|
||||
import { Request } from '../../../../models/request';
|
||||
import { RequestLoaderData } from '../../../routes/request';
|
||||
import { ApiKeyAuth } from './api-key-auth';
|
||||
import { AsapAuth } from './asap-auth';
|
||||
@@ -29,7 +28,7 @@ import { OAuth1Auth } from './o-auth-1-auth';
|
||||
import { OAuth2Auth } from './o-auth-2-auth';
|
||||
|
||||
export const AuthWrapper: FC<{ disabled?: boolean }> = ({ disabled = false }) => {
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
|
||||
const { authentication: { type } } = activeRequest;
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import classnames from 'classnames';
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { Request } from '../../../../../models/request';
|
||||
import { RequestAccordionKeys, RequestMeta } from '../../../../../models/request-meta';
|
||||
import { RequestAccordionKeys } from '../../../../../models/request-meta';
|
||||
import { useRequestMetaPatcher } from '../../../../hooks/use-request';
|
||||
import { RequestLoaderData } from '../../../../routes/request';
|
||||
|
||||
@@ -13,7 +12,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const AuthAccordion: FC<PropsWithChildren<Props>> = ({ accordionKey, label, children }) => {
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
|
||||
const expanded = Boolean(activeRequestMeta?.expandedAccordionKeys[accordionKey]);
|
||||
const patchRequestMeta = useRequestMetaPatcher();
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useRouteLoaderData } from 'react-router-dom';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { toKebabCase } from '../../../../../common/misc';
|
||||
import { Request } from '../../../../../models/request';
|
||||
import { useRequestPatcher } from '../../../../hooks/use-request';
|
||||
import { RequestLoaderData } from '../../../../routes/request';
|
||||
import { RootLoaderData } from '../../../../routes/root';
|
||||
@@ -23,7 +22,7 @@ export const AuthInputRow: FC<Props> = ({ label, getAutocompleteConstants, prope
|
||||
settings,
|
||||
} = useRouteLoaderData('root') as RootLoaderData;
|
||||
const { showPasswords } = settings;
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const patchRequest = useRequestPatcher();
|
||||
const [masked, toggleMask] = useToggle(true);
|
||||
const canBeMasked = !showPasswords && mask;
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { FC, ReactNode, useCallback } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { toKebabCase } from '../../../../../common/misc';
|
||||
import { Request } from '../../../../../models/request';
|
||||
import { useNunjucks } from '../../../../context/nunjucks/use-nunjucks';
|
||||
import { useRequestPatcher } from '../../../../hooks/use-request';
|
||||
import { RequestLoaderData } from '../../../../routes/request';
|
||||
@@ -30,7 +29,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const AuthPrivateKeyRow: FC<Props> = ({ label, property, help }) => {
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const patchRequest = useRequestPatcher();
|
||||
const { handleGetRenderContext, handleRender } = useNunjucks();
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import classnames from 'classnames';
|
||||
import React, { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { Request } from '../../../../../models/request';
|
||||
import { RequestLoaderData } from '../../../../routes/request';
|
||||
import { HelpTooltip } from '../../../help-tooltip';
|
||||
|
||||
@@ -14,7 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const AuthRow: FC<PropsWithChildren<Props>> = ({ labelFor, label, help, disabled, children }) => {
|
||||
const { activeRequest: { authentication } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest: { authentication } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
|
||||
return (
|
||||
<tr key={labelFor}>
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { ChangeEvent, FC, ReactNode, useCallback } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { toKebabCase } from '../../../../../common/misc';
|
||||
import { Request } from '../../../../../models/request';
|
||||
import { useRequestPatcher } from '../../../../hooks/use-request';
|
||||
import { RequestLoaderData } from '../../../../routes/request';
|
||||
import { AuthRow } from './auth-row';
|
||||
@@ -19,7 +18,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const AuthSelectRow: FC<Props> = ({ label, property, help, options, disabled }) => {
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const patchRequest = useRequestPatcher();
|
||||
|
||||
const selectedValue = authentication.hasOwnProperty(property) ? authentication[property] : options[0].value;
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { FC, ReactNode, useCallback } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { toKebabCase } from '../../../../../common/misc';
|
||||
import { Request } from '../../../../../models/request';
|
||||
import { useRequestPatcher } from '../../../../hooks/use-request';
|
||||
import { RequestLoaderData } from '../../../../routes/request';
|
||||
import { AuthRow } from './auth-row';
|
||||
@@ -28,7 +27,7 @@ export const AuthToggleRow: FC<Props> = ({
|
||||
offTitle = 'Enable item',
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const patchRequest = useRequestPatcher();
|
||||
|
||||
const databaseValue = Boolean(authentication[property]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { Request } from '../../../../models/request';
|
||||
import {
|
||||
OAuth1SignatureMethod,
|
||||
SIGNATURE_METHOD_HMAC_SHA1,
|
||||
@@ -35,7 +34,7 @@ const signatureMethodOptions: {name: string; value: OAuth1SignatureMethod}[] = [
|
||||
}];
|
||||
|
||||
export const OAuth1Auth: FC = () => {
|
||||
const { activeRequest: { authentication: { signatureMethod } } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest: { authentication: { signatureMethod } } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
|
||||
return (
|
||||
<AuthTableBody>
|
||||
|
||||
@@ -244,7 +244,7 @@ const getFieldsForGrantType = (authentication: Request['authentication']) => {
|
||||
};
|
||||
|
||||
export const OAuth2Auth: FC = () => {
|
||||
const { activeRequest: { authentication } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest: { authentication } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
|
||||
const { basic, advanced } = getFieldsForGrantType(authentication);
|
||||
|
||||
@@ -331,7 +331,7 @@ const renderAccessTokenExpiry = (token?: Pick<OAuth2Token, 'accessToken' | 'expi
|
||||
};
|
||||
|
||||
const OAuth2TokenInput: FC<{ token: OAuth2Token | null; label: string; property: keyof Pick<OAuth2Token, 'accessToken' | 'refreshToken' | 'identityToken'> }> = ({ token, label, property }) => {
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
|
||||
const onChange = async ({ currentTarget: { value } }: ChangeEvent<HTMLInputElement>) => {
|
||||
if (token) {
|
||||
@@ -414,7 +414,7 @@ const OAuth2Error: FC<{ token: OAuth2Token | null }> = ({ token }) => {
|
||||
};
|
||||
|
||||
const OAuth2Tokens: FC = () => {
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const [token, setToken] = useState<OAuth2Token | null>(null);
|
||||
useEffect(() => {
|
||||
const fn = async () => {
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, { FC, useCallback } from 'react';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { getCommonHeaderNames, getCommonHeaderValues } from '../../../common/common-headers';
|
||||
import type { Request, RequestHeader } from '../../../models/request';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import type { RequestHeader } from '../../../models/request';
|
||||
import { isWebSocketRequest } from '../../../models/websocket-request';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { RequestLoaderData, WebSocketRequestLoaderData } from '../../routes/request';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { KeyValueEditor } from '../key-value-editor/key-value-editor';
|
||||
|
||||
@@ -18,7 +18,7 @@ export const RequestHeadersEditor: FC<Props> = ({
|
||||
bulk,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request | WebSocketRequest, any>;
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData;
|
||||
const patchRequest = useRequestPatcher();
|
||||
const { requestId } = useParams() as { requestId: string };
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { Request, RequestParameter } from '../../../models/request';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { RequestParameter } from '../../../models/request';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { RequestLoaderData, WebSocketRequestLoaderData } from '../../routes/request';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { KeyValueEditor } from '../key-value-editor/key-value-editor';
|
||||
|
||||
@@ -18,7 +17,7 @@ export const RequestParametersEditor: FC<Props> = ({
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { requestId } = useParams() as { requestId: string };
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request | WebSocketRequest, any>;
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData;
|
||||
const patchRequest = useRequestPatcher();
|
||||
const handleBulkUpdate = useCallback((paramsString: string) => {
|
||||
const parameters: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import clone from 'clone';
|
||||
import { isValid } from 'date-fns';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
import { Cookie as ToughCookie } from 'tough-cookie';
|
||||
@@ -17,29 +18,16 @@ import { OneLineEditor } from '../codemirror/one-line-editor';
|
||||
export interface CookieModifyModalOptions {
|
||||
cookie: Cookie;
|
||||
}
|
||||
export interface CookieModifyModalHandle {
|
||||
show: (options: CookieModifyModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const CookieModifyModal = forwardRef<CookieModifyModalHandle, ModalProps>((_, ref) => {
|
||||
|
||||
export const CookieModifyModal = ((props: ModalProps & CookieModifyModalOptions) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [cookie, setCookie] = useState<Cookie | null>(null);
|
||||
const [cookie, setCookie] = useState<Cookie | null>(props.cookie);
|
||||
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const updateCookieJarFetcher = useFetcher<CookieJar>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: ({ cookie }) => {
|
||||
if (!activeCookieJar.cookies.find(c => c.id === cookie.id)) {
|
||||
return;
|
||||
}
|
||||
setCookie(cookie);
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), [activeCookieJar.cookies]);
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
const updateCookieJar = async (cookieJarId: string, patch: CookieJar) => {
|
||||
updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), {
|
||||
encType: 'application/json',
|
||||
@@ -88,119 +76,120 @@ export const CookieModifyModal = forwardRef<CookieModifyModalHandle, ModalProps>
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>Edit Cookie</ModalHeader>
|
||||
<ModalBody className="cookie-modify">
|
||||
{activeCookieJar && cookie && (
|
||||
<Tabs aria-label="Cookie modify tabs">
|
||||
<TabItem key="friendly" title="Friendly">
|
||||
<PanelContainer className="pad">
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookieKey">
|
||||
Key
|
||||
<OneLineEditor
|
||||
defaultValue={(cookie && cookie.key || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { key: value.trim() }))}
|
||||
/>
|
||||
</label>
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} onHide={props.onHide}>
|
||||
<ModalHeader>Edit Cookie</ModalHeader>
|
||||
<ModalBody className="cookie-modify">
|
||||
{activeCookieJar && cookie && (
|
||||
<Tabs aria-label="Cookie modify tabs">
|
||||
<TabItem key="friendly" title="Friendly">
|
||||
<PanelContainer className="pad">
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookieKey">
|
||||
Key
|
||||
<OneLineEditor
|
||||
defaultValue={(cookie && cookie.key || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { key: value.trim() }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookieValue">
|
||||
Value
|
||||
<OneLineEditor
|
||||
defaultValue={(cookie && cookie.value || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { value: value.trim() }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookieDomain">
|
||||
Domain
|
||||
<OneLineEditor
|
||||
defaultValue={(cookie && cookie.domain || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { domain: value.trim() }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookiePath">
|
||||
Path
|
||||
<OneLineEditor
|
||||
defaultValue={(cookie && cookie.path || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { path: value.trim() }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookieValue">
|
||||
Value
|
||||
<OneLineEditor
|
||||
defaultValue={(cookie && cookie.value || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { value: value.trim() }))}
|
||||
/>
|
||||
<label data-testid="CookieExpires">
|
||||
Expires
|
||||
<input type="datetime-local" defaultValue={localDateTime} onChange={event => handleCookieUpdate(Object.assign({}, cookie, { expires: event.target.value }))} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookieDomain">
|
||||
Domain
|
||||
<OneLineEditor
|
||||
defaultValue={(cookie && cookie.domain || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { domain: value.trim() }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookiePath">
|
||||
Path
|
||||
<OneLineEditor
|
||||
defaultValue={(cookie && cookie.path || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { path: value.trim() }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label data-testid="CookieExpires">
|
||||
Expires
|
||||
<input type="datetime-local" defaultValue={localDateTime} onChange={event => handleCookieUpdate(Object.assign({}, cookie, { expires: event.target.value }))} />
|
||||
</label>
|
||||
</div>
|
||||
</PanelContainer>
|
||||
<div className="pad no-pad-top cookie-modify__checkboxes row-around txt-lg">
|
||||
<label>
|
||||
Secure
|
||||
<input
|
||||
className="space-left"
|
||||
type="checkbox"
|
||||
name="secure"
|
||||
defaultChecked={cookie.secure || false}
|
||||
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { secure: event.target.checked }))}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
httpOnly
|
||||
<input
|
||||
className="space-left"
|
||||
type="checkbox"
|
||||
name="httpOnly"
|
||||
defaultChecked={cookie.httpOnly || false}
|
||||
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { httpOnly: event.target.checked }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</TabItem>
|
||||
<TabItem key="raw" title="Raw">
|
||||
<PanelContainer className="pad">
|
||||
<div className="form-control form-control--outlined">
|
||||
</PanelContainer>
|
||||
<div className="pad no-pad-top cookie-modify__checkboxes row-around txt-lg">
|
||||
<label>
|
||||
Raw Cookie String
|
||||
Secure
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => {
|
||||
try {
|
||||
// NOTE: Perform toJSON so we have a plain JS object instead of Cookie instance
|
||||
const parsed = ToughCookie.parse(event.target.value)?.toJSON();
|
||||
if (parsed) {
|
||||
// Make sure cookie has an id
|
||||
parsed.id = cookie.id;
|
||||
handleCookieUpdate(parsed);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to parse cookie string "${event.target.value}"`, err);
|
||||
return;
|
||||
}
|
||||
}}
|
||||
defaultValue={rawDefaultValue}
|
||||
className="space-left"
|
||||
type="checkbox"
|
||||
name="secure"
|
||||
defaultChecked={cookie.secure || false}
|
||||
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { secure: event.target.checked }))}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
httpOnly
|
||||
<input
|
||||
className="space-left"
|
||||
type="checkbox"
|
||||
name="httpOnly"
|
||||
defaultChecked={cookie.httpOnly || false}
|
||||
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { httpOnly: event.target.checked }))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Done
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</TabItem>
|
||||
<TabItem key="raw" title="Raw">
|
||||
<PanelContainer className="pad">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Raw Cookie String
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => {
|
||||
try {
|
||||
// NOTE: Perform toJSON so we have a plain JS object instead of Cookie instance
|
||||
const parsed = ToughCookie.parse(event.target.value)?.toJSON();
|
||||
if (parsed) {
|
||||
// Make sure cookie has an id
|
||||
parsed.id = cookie.id;
|
||||
handleCookieUpdate(parsed);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to parse cookie string "${event.target.value}"`, err);
|
||||
return;
|
||||
}
|
||||
}}
|
||||
defaultValue={rawDefaultValue}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Done
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
CookieModifyModal.displayName = 'CookieModifyModal';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { fuzzyMatch } from '../../../common/misc';
|
||||
@@ -10,12 +11,8 @@ import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { CookieList } from '../cookie-list';
|
||||
import { showModal } from '.';
|
||||
export interface CookiesModalHandle {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const CookiesModal = forwardRef<CookiesModalHandle, ModalProps>((_, ref) => {
|
||||
|
||||
export const CookiesModal = ({ onHide }: ModalProps) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const { handleRender } = useNunjucks();
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
@@ -23,15 +20,10 @@ export const CookiesModal = forwardRef<CookiesModalHandle, ModalProps>((_, ref)
|
||||
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const updateCookieJarFetcher = useFetcher<CookieJar>();
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: () => {
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), []);
|
||||
const updateCookieJar = async (cookieJarId: string, patch: CookieJar) => {
|
||||
updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), {
|
||||
encType: 'application/json',
|
||||
@@ -41,80 +33,79 @@ export const CookiesModal = forwardRef<CookiesModalHandle, ModalProps>((_, ref)
|
||||
};
|
||||
const filteredCookies = visibleCookieIndexes ? (activeCookieJar?.cookies || []).filter((_, i) => visibleCookieIndexes.includes(i)) : (activeCookieJar?.cookies || []);
|
||||
return (
|
||||
<Modal ref={modalRef} wide tall>
|
||||
<ModalHeader>Manage Cookies</ModalHeader>
|
||||
<ModalBody noScroll>
|
||||
{activeCookieJar && (
|
||||
<div className="cookie-list">
|
||||
<div className="pad">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Filter Cookies
|
||||
<input
|
||||
onChange={async event => {
|
||||
setFilter(event.target.value);
|
||||
const renderedCookies: Cookie[] = [];
|
||||
for (const cookie of (activeCookieJar?.cookies || [])) {
|
||||
try {
|
||||
renderedCookies.push(await handleRender(cookie));
|
||||
} catch (err) {
|
||||
renderedCookies.push(cookie);
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} wide tall onHide={onHide}>
|
||||
<ModalHeader>Manage Cookies</ModalHeader>
|
||||
<ModalBody noScroll>
|
||||
{activeCookieJar && (
|
||||
<div className="cookie-list">
|
||||
<div className="pad">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Filter Cookies
|
||||
<input
|
||||
onChange={async event => {
|
||||
setFilter(event.target.value);
|
||||
const renderedCookies: Cookie[] = [];
|
||||
for (const cookie of (activeCookieJar?.cookies || [])) {
|
||||
try {
|
||||
renderedCookies.push(await handleRender(cookie));
|
||||
} catch (err) {
|
||||
renderedCookies.push(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!filter) {
|
||||
setVisibleCookieIndexes(null);
|
||||
}
|
||||
const visibleCookieIndexes: number[] = [];
|
||||
renderedCookies.forEach((cookie, i) => {
|
||||
if (fuzzyMatch(filter, JSON.stringify(cookie), { splitSpace: true })) {
|
||||
visibleCookieIndexes.push(i);
|
||||
if (!filter) {
|
||||
setVisibleCookieIndexes(null);
|
||||
}
|
||||
});
|
||||
setVisibleCookieIndexes(visibleCookieIndexes);
|
||||
}}
|
||||
type="text"
|
||||
placeholder="insomnia.rest"
|
||||
defaultValue=""
|
||||
/>
|
||||
</label>
|
||||
const visibleCookieIndexes: number[] = [];
|
||||
renderedCookies.forEach((cookie, i) => {
|
||||
if (fuzzyMatch(filter, JSON.stringify(cookie), { splitSpace: true })) {
|
||||
visibleCookieIndexes.push(i);
|
||||
}
|
||||
});
|
||||
setVisibleCookieIndexes(visibleCookieIndexes);
|
||||
}}
|
||||
type="text"
|
||||
placeholder="insomnia.rest"
|
||||
defaultValue=""
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cookie-list__list border-tops pad">
|
||||
<CookieList
|
||||
cookies={filteredCookies}
|
||||
handleDeleteAll={() => {
|
||||
const updated = activeCookieJar;
|
||||
updated.cookies = [];
|
||||
updateCookieJar(activeCookieJar._id, updated);
|
||||
}}
|
||||
handleCookieAdd={cookie => {
|
||||
const updated = activeCookieJar;
|
||||
updated.cookies = [cookie, ...activeCookieJar.cookies];
|
||||
updateCookieJar(activeCookieJar._id, updated);
|
||||
}}
|
||||
handleCookieDelete={cookie => {
|
||||
const updated = activeCookieJar;
|
||||
updated.cookies = activeCookieJar.cookies.filter(c => c.id !== cookie.id);
|
||||
updateCookieJar(activeCookieJar._id, updated);
|
||||
}}
|
||||
// Set the domain to the filter so that it shows up if we're filtering
|
||||
newCookieDomainName={filter || 'domain.com'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cookie-list__list border-tops pad">
|
||||
<CookieList
|
||||
cookies={filteredCookies}
|
||||
handleDeleteAll={() => {
|
||||
const updated = activeCookieJar;
|
||||
updated.cookies = [];
|
||||
updateCookieJar(activeCookieJar._id, updated);
|
||||
}}
|
||||
handleCookieAdd={cookie => {
|
||||
const updated = activeCookieJar;
|
||||
updated.cookies = [cookie, ...activeCookieJar.cookies];
|
||||
updateCookieJar(activeCookieJar._id, updated);
|
||||
}}
|
||||
handleCookieDelete={cookie => {
|
||||
const updated = activeCookieJar;
|
||||
updated.cookies = activeCookieJar.cookies.filter(c => c.id !== cookie.id);
|
||||
updateCookieJar(activeCookieJar._id, updated);
|
||||
}}
|
||||
// Set the domain to the filter so that it shows up if we're filtering
|
||||
newCookieDomainName={filter || 'domain.com'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left faint italic txt-sm">
|
||||
* cookies are automatically sent with relevant requests
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left faint italic txt-sm">
|
||||
* cookies are automatically sent with relevant requests
|
||||
</div>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Done
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Done
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
CookiesModal.displayName = 'CookiesModal';
|
||||
|
||||
export const showCookiesModal = () => showModal(CookiesModal);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { exportRequestsToFile } from '../../../common/export';
|
||||
import * as models from '../../../models';
|
||||
@@ -7,7 +8,7 @@ import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { selectSidebarChildren } from '../../redux/selectors';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
@@ -29,10 +30,13 @@ export interface ExportRequestsModalHandle {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const ExportRequestsModal = forwardRef<ExportRequestsModalHandle, ModalProps>((props, ref) => {
|
||||
|
||||
export const ExportRequestsModal = ({ onHide }: ModalProps) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<State>({ treeRoot: null });
|
||||
const sidebarChildren = useSelector(selectSidebarChildren);
|
||||
const {
|
||||
requestTree,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
|
||||
const createNode = useCallback((item: Record<string, any>): Node => {
|
||||
const children: Node[] = item.children.map((child: Record<string, any>) => createNode(child));
|
||||
let totalRequests = children
|
||||
@@ -50,39 +54,35 @@ export const ExportRequestsModal = forwardRef<ExportRequestsModalHandle, ModalPr
|
||||
selectedRequests: totalRequests, // Default select all
|
||||
};
|
||||
}, []);
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: () => {
|
||||
modalRef.current?.show();
|
||||
const childObjects = sidebarChildren.all;
|
||||
const children: Node[] = childObjects.map(child => createNode(child));
|
||||
const totalRequests = children
|
||||
.map(child => child.totalRequests)
|
||||
.reduce((acc, totalRequests) => acc + totalRequests, 0);
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION missing property
|
||||
const rootFolder: RequestGroup = {
|
||||
...models.requestGroup.init(),
|
||||
_id: 'all',
|
||||
type: models.requestGroup.type,
|
||||
name: 'All requests',
|
||||
parentId: '',
|
||||
modified: 0,
|
||||
created: 0,
|
||||
};
|
||||
setState({
|
||||
treeRoot: {
|
||||
doc: rootFolder,
|
||||
collapsed: false,
|
||||
children: children,
|
||||
totalRequests: totalRequests,
|
||||
selectedRequests: totalRequests, // Default select all
|
||||
},
|
||||
});
|
||||
const children: Node[] = requestTree.map(child => createNode(child));
|
||||
const totalRequests = children
|
||||
.map(child => child.totalRequests)
|
||||
.reduce((acc, totalRequests) => acc + totalRequests, 0);
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION missing property
|
||||
const rootFolder: RequestGroup = {
|
||||
...models.requestGroup.init(),
|
||||
_id: 'all',
|
||||
type: models.requestGroup.type,
|
||||
name: 'All requests',
|
||||
parentId: '',
|
||||
modified: 0,
|
||||
created: 0,
|
||||
};
|
||||
|
||||
const [state, setState] = useState<State>({
|
||||
treeRoot: {
|
||||
doc: rootFolder,
|
||||
collapsed: false,
|
||||
children: children,
|
||||
totalRequests: totalRequests,
|
||||
selectedRequests: totalRequests, // Default select all
|
||||
},
|
||||
}), [createNode, sidebarChildren.all]);
|
||||
});
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
const getSelectedRequestIds = (node: Node): string[] => {
|
||||
const docIsRequest = isRequest(node.doc) || isWebSocketRequest(node.doc) || isGrpcRequest(node.doc);
|
||||
if (docIsRequest && node.selectedRequests === node.totalRequests) {
|
||||
@@ -168,32 +168,33 @@ export const ExportRequestsModal = forwardRef<ExportRequestsModalHandle, ModalPr
|
||||
const { treeRoot } = state;
|
||||
const isExportDisabled = treeRoot != null ? treeRoot.selectedRequests === 0 : false;
|
||||
return (
|
||||
<Modal ref={modalRef} tall {...props}>
|
||||
<ModalHeader>Select Requests to Export</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="requests-tree">
|
||||
<Tree
|
||||
root={treeRoot}
|
||||
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
|
||||
handleSetItemSelected={handleSetItemSelected}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleExport}
|
||||
disabled={isExportDisabled}
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} tall onHide={onHide}>
|
||||
<ModalHeader>Select Requests to Export</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="requests-tree">
|
||||
<Tree
|
||||
root={treeRoot}
|
||||
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
|
||||
handleSetItemSelected={handleSetItemSelected}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleExport}
|
||||
disabled={isExportDisabled}
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
ExportRequestsModal.displayName = 'ExportRequestsModal';
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export const ProjectSettingsModal: FC<ProjectSettingsModalProps> = ({ project, o
|
||||
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isRemote = isRemoteProject(project);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
|
||||
import { database as db } from '../../../common/database';
|
||||
import * as models from '../../../models';
|
||||
import type { RequestGroup } from '../../../models/request-group';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { invariant } from '../../../utils/invariant';
|
||||
import { useRequestGroupPatcher } from '../../hooks/use-request';
|
||||
import { selectWorkspacesForActiveProject } from '../../redux/selectors';
|
||||
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
@@ -16,127 +16,81 @@ import { MarkdownEditor } from '../markdown-editor';
|
||||
|
||||
export interface RequestGroupSettingsModalOptions {
|
||||
requestGroup: RequestGroup;
|
||||
forceEditMode?: boolean;
|
||||
}
|
||||
interface State {
|
||||
requestGroup: RequestGroup | null;
|
||||
showDescription: boolean;
|
||||
defaultPreviewMode: boolean;
|
||||
activeWorkspaceIdToCopyTo: string | null;
|
||||
workspace?: Workspace;
|
||||
workspacesForActiveProject: Workspace[];
|
||||
}
|
||||
export interface RequestGroupSettingsModalHandle {
|
||||
show: (options: RequestGroupSettingsModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const RequestGroupSettingsModal = forwardRef<RequestGroupSettingsModalHandle, ModalProps>((_, ref) => {
|
||||
export const RequestGroupSettingsModal = ({ requestGroup, onHide }: ModalProps & {
|
||||
requestGroup: RequestGroup;
|
||||
}) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const editorRef = useRef<CodeEditorHandle>(null);
|
||||
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
|
||||
const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string };
|
||||
|
||||
const [state, setState] = useState<State>({
|
||||
requestGroup: null,
|
||||
showDescription: false,
|
||||
defaultPreviewMode: false,
|
||||
activeWorkspaceIdToCopyTo: null,
|
||||
workspace: undefined,
|
||||
workspacesForActiveProject: [],
|
||||
defaultPreviewMode: !!requestGroup.description,
|
||||
});
|
||||
const patchRequestGroup = useRequestGroupPatcher();
|
||||
const requestFetcher = useFetcher();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async ({ requestGroup, forceEditMode }) => {
|
||||
const hasDescription = !!requestGroup.description;
|
||||
// Find this request workspace for filtering out of workspaces list
|
||||
const ancestors = await db.withAncestors(requestGroup);
|
||||
const workspace = workspacesForActiveProject
|
||||
.find(w => w._id === ancestors.find(doc => doc.type === models.workspace.type)?._id);
|
||||
|
||||
setState(state => ({
|
||||
...state,
|
||||
requestGroup,
|
||||
workspace,
|
||||
activeWorkspaceIdToCopyTo: null,
|
||||
showDescription: forceEditMode || hasDescription,
|
||||
defaultPreviewMode: hasDescription && !forceEditMode,
|
||||
}));
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), [workspacesForActiveProject]);
|
||||
const duplicateRequestGroup = (r: Partial<RequestGroup>) => {
|
||||
requestFetcher.submit(JSON.stringify(r),
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${requestGroup._id}/duplicate`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
const handleMoveToWorkspace = async () => {
|
||||
const { activeWorkspaceIdToCopyTo, requestGroup } = state;
|
||||
if (!requestGroup || !activeWorkspaceIdToCopyTo) {
|
||||
return;
|
||||
}
|
||||
const workspace = await models.workspace.getById(activeWorkspaceIdToCopyTo);
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
// TODO: if there are gRPC requests in a request group
|
||||
// we should also copy the protofiles to the destination workspace - INS-267
|
||||
await models.requestGroup.duplicate(requestGroup, {
|
||||
metaSortKey: -1e9,
|
||||
parentId: activeWorkspaceIdToCopyTo,
|
||||
name: requestGroup.name, // Because duplicating will add (Copy) suffix
|
||||
});
|
||||
// TODO clean up this so it doesn't orphan descendants
|
||||
await models.requestGroup.remove(requestGroup);
|
||||
invariant(state.activeWorkspaceIdToCopyTo, 'Workspace ID is required');
|
||||
patchRequestGroup(requestGroup._id, { parentId: state.activeWorkspaceIdToCopyTo });
|
||||
modalRef.current?.hide();
|
||||
};
|
||||
|
||||
const handleCopyToWorkspace = async () => {
|
||||
const { activeWorkspaceIdToCopyTo, requestGroup } = state;
|
||||
if (!requestGroup || !activeWorkspaceIdToCopyTo) {
|
||||
return;
|
||||
}
|
||||
const workspace = await models.workspace.getById(activeWorkspaceIdToCopyTo);
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
const patch = {
|
||||
invariant(state.activeWorkspaceIdToCopyTo, 'Workspace ID is required');
|
||||
duplicateRequestGroup({
|
||||
metaSortKey: -1e9, // Move to top of sort order
|
||||
name: requestGroup.name, // Because duplicate will add (Copy) suffix if name is not provided in patch
|
||||
parentId: activeWorkspaceIdToCopyTo,
|
||||
};
|
||||
await models.requestGroup.duplicate(requestGroup, patch);
|
||||
models.stats.incrementCreatedRequests();
|
||||
parentId: state.activeWorkspaceIdToCopyTo,
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
requestGroup,
|
||||
showDescription,
|
||||
defaultPreviewMode,
|
||||
activeWorkspaceIdToCopyTo,
|
||||
workspace,
|
||||
} = state;
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>
|
||||
Folder Settings{' '}
|
||||
<span className="txt-sm selectable faint monospace">
|
||||
{requestGroup?._id || ''}
|
||||
</span>
|
||||
</ModalHeader>
|
||||
<ModalBody className="pad"><div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
placeholder={requestGroup?.name || 'My Folder'}
|
||||
defaultValue={requestGroup?.name}
|
||||
onChange={async event => {
|
||||
invariant(requestGroup, 'No request group');
|
||||
const updatedRequestGroup = await models.requestGroup.update(requestGroup, { name: event.target.value });
|
||||
setState(state => ({ ...state, requestGroup: updatedRequestGroup }));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{showDescription ? (
|
||||
<OverlayContainer onClick={e => e.stopPropagation()}>
|
||||
<Modal ref={modalRef} onHide={onHide}>
|
||||
<ModalHeader>
|
||||
Folder Settings{' '}
|
||||
<span className="txt-sm selectable faint monospace">
|
||||
{requestGroup?._id || ''}
|
||||
</span>
|
||||
</ModalHeader>
|
||||
<ModalBody className="pad"><div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
placeholder={requestGroup?.name || 'My Folder'}
|
||||
defaultValue={requestGroup?.name}
|
||||
onChange={async event => {
|
||||
invariant(requestGroup, 'No request group');
|
||||
patchRequestGroup(requestGroup._id, { name: event.target.value });
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<MarkdownEditor
|
||||
ref={editorRef}
|
||||
className="margin-top"
|
||||
@@ -145,66 +99,54 @@ export const RequestGroupSettingsModal = forwardRef<RequestGroupSettingsModalHan
|
||||
defaultValue={requestGroup?.description || ''}
|
||||
onChange={async (description: string) => {
|
||||
invariant(requestGroup, 'No request group');
|
||||
const updated = await models.requestGroup.update(requestGroup, { description });
|
||||
setState(state => ({ ...state, requestGroup: updated, defaultPreviewMode: false }));
|
||||
patchRequestGroup(requestGroup._id, { description });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setState(state => ({ ...state, showDescription: true }))}
|
||||
className="btn btn--outlined btn--super-duper-compact"
|
||||
>
|
||||
Add Description
|
||||
</button>
|
||||
)}
|
||||
<hr />
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Move/Copy to Workspace
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Copy or move the current folder to a new workspace. It will be
|
||||
placed at the root of the new workspace's folder structure.
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={activeWorkspaceIdToCopyTo || '__NULL__'}
|
||||
onChange={event => {
|
||||
const workspaceId = event.currentTarget.value === '__NULL__' ? null : event.currentTarget.value;
|
||||
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
|
||||
}}
|
||||
<hr />
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Move/Copy to Workspace
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Copy or move the current folder to a new workspace. It will be
|
||||
placed at the root of the new workspace's folder structure.
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={activeWorkspaceIdToCopyTo || '__NULL__'}
|
||||
onChange={event => setState(state => ({ ...state, activeWorkspaceIdToCopyTo: event.currentTarget.value === '__NULL__' ? null : event.currentTarget.value }))}
|
||||
>
|
||||
<option value="__NULL__">-- Select Workspace --</option>
|
||||
{workspacesForActiveProject
|
||||
.filter(w => workspaceId !== w._id)
|
||||
.map(w => (
|
||||
<option key={w._id} value={w._id}>
|
||||
{w.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleCopyToWorkspace}
|
||||
>
|
||||
<option value="__NULL__">-- Select Workspace --</option>
|
||||
{workspacesForActiveProject
|
||||
.filter(w => workspace?._id !== w._id)
|
||||
.map(w => (
|
||||
<option key={w._id} value={w._id}>
|
||||
{w.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleMoveToWorkspace}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleCopyToWorkspace}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleMoveToWorkspace}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div></ModalBody>
|
||||
</Modal>
|
||||
</div></ModalBody>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
RequestGroupSettingsModal.displayName = 'RequestGroupSettingsModal';
|
||||
};
|
||||
|
||||
@@ -4,15 +4,12 @@ import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { docsTemplateTags } from '../../../common/documentation';
|
||||
import { GrpcRequest } from '../../../models/grpc-request';
|
||||
import { Request } from '../../../models/request';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { RenderError } from '../../../templating';
|
||||
import { Link } from '../base/link';
|
||||
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { RequestSettingsModal } from '../modals/request-settings-modal';
|
||||
import { showModal } from './index';
|
||||
export interface RequestRenderErrorModalOptions {
|
||||
error: RenderError | null;
|
||||
request: Request | WebSocketRequest | GrpcRequest | null;
|
||||
@@ -55,17 +52,6 @@ export const RequestRenderErrorModal = forwardRef<RequestRenderErrorModalHandle,
|
||||
Failed to render <strong>{fullPath}</strong> prior to sending
|
||||
</p>
|
||||
<div className="pad-top-sm">
|
||||
{error.path?.match(/^body/) && isRequest(request) && (
|
||||
<button
|
||||
className="btn btn--clicky margin-right-sm"
|
||||
onClick={() => {
|
||||
modalRef.current?.hide();
|
||||
showModal(RequestSettingsModal, { request });
|
||||
}}
|
||||
>
|
||||
Adjust Render Settings
|
||||
</button>
|
||||
)}
|
||||
<Link button href={docsTemplateTags} className="btn btn--clicky">
|
||||
Templating Documentation <i className="fa fa-external-link" />
|
||||
</Link>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
|
||||
import { database as db } from '../../../common/database';
|
||||
import * as models from '../../../models';
|
||||
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { isWorkspace, Workspace } from '../../../models/workspace';
|
||||
import { invariant } from '../../../utils/invariant';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { selectWorkspacesForActiveProject } from '../../redux/selectors';
|
||||
@@ -20,413 +19,346 @@ import { MarkdownEditor } from '../markdown-editor';
|
||||
|
||||
export interface RequestSettingsModalOptions {
|
||||
request: Request | GrpcRequest | WebSocketRequest;
|
||||
forceEditMode?: boolean;
|
||||
}
|
||||
interface State {
|
||||
request: Request | GrpcRequest | WebSocketRequest | null;
|
||||
showDescription: boolean;
|
||||
defaultPreviewMode: boolean;
|
||||
activeWorkspaceIdToCopyTo: string | null;
|
||||
workspace?: Workspace;
|
||||
}
|
||||
export interface RequestSettingsModalHandle {
|
||||
show: (options: RequestSettingsModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, ModalProps>((_, ref) => {
|
||||
export const RequestSettingsModal = ({ request, onHide }: ModalProps & RequestSettingsModalOptions) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const editorRef = useRef<CodeEditorHandle>(null);
|
||||
|
||||
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
|
||||
const [state, setState] = useState<State>({
|
||||
request: null,
|
||||
showDescription: false,
|
||||
defaultPreviewMode: false,
|
||||
defaultPreviewMode: !!request?.description,
|
||||
activeWorkspaceIdToCopyTo: null,
|
||||
workspace: undefined,
|
||||
});
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
const requestFetcher = useFetcher();
|
||||
const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string };
|
||||
const patchRequest = useRequestPatcher();
|
||||
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async ({ request, forceEditMode }) => {
|
||||
const hasDescription = !!request.description;
|
||||
// Find this request workspace for filtering out of workspaces list
|
||||
const ancestors = await db.withAncestors(request);
|
||||
const workspace = workspacesForActiveProject.find(w => w._id === ancestors.find(isWorkspace)?._id);
|
||||
setState(state => ({
|
||||
...state,
|
||||
request,
|
||||
workspace: workspace,
|
||||
activeWorkspaceIdToCopyTo: null,
|
||||
showDescription: forceEditMode || hasDescription,
|
||||
defaultPreviewMode: hasDescription && !forceEditMode,
|
||||
}));
|
||||
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), [workspacesForActiveProject]);
|
||||
|
||||
const updateRequest = (req: Partial<Request>) => {
|
||||
invariant(state.request, 'Request is required');
|
||||
patchRequest(state.request._id, req);
|
||||
};
|
||||
const duplicateRequest = (r: Partial<Request>) => {
|
||||
invariant(state.request, 'Request is required');
|
||||
requestFetcher.submit(JSON.stringify(r),
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${state.request._id}/duplicate`,
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${request._id}/duplicate`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
async function handleMoveToWorkspace() {
|
||||
const { activeWorkspaceIdToCopyTo } = state;
|
||||
invariant(activeWorkspaceIdToCopyTo, 'Workspace ID is required');
|
||||
updateRequest({ parentId: activeWorkspaceIdToCopyTo });
|
||||
invariant(state.activeWorkspaceIdToCopyTo, 'Workspace ID is required');
|
||||
patchRequest(request._id, { parentId: state.activeWorkspaceIdToCopyTo });
|
||||
modalRef.current?.hide();
|
||||
}
|
||||
|
||||
async function handleCopyToWorkspace() {
|
||||
const { activeWorkspaceIdToCopyTo } = state;
|
||||
invariant(activeWorkspaceIdToCopyTo, 'Workspace ID is required');
|
||||
duplicateRequest({ parentId: activeWorkspaceIdToCopyTo });
|
||||
invariant(state.activeWorkspaceIdToCopyTo, 'Workspace ID is required');
|
||||
duplicateRequest({ parentId: state.activeWorkspaceIdToCopyTo });
|
||||
}
|
||||
const { request, showDescription, defaultPreviewMode, activeWorkspaceIdToCopyTo, workspace } = state;
|
||||
const { defaultPreviewMode, activeWorkspaceIdToCopyTo } = state;
|
||||
const toggleCheckBox = async (event: any) => {
|
||||
updateRequest({ [event.currentTarget.name]: event.currentTarget.checked ? true : false });
|
||||
|
||||
const updated = { ...state.request, [event.currentTarget.name]: event.currentTarget.checked } as Request;
|
||||
setState(state => ({ ...state, request: updated }));
|
||||
patchRequest(request._id, { [event.currentTarget.name]: event.currentTarget.checked ? true : false });
|
||||
};
|
||||
const updateDescription = (description: string) => {
|
||||
updateRequest({ description });
|
||||
const updated = { ...state.request, description } as Request;
|
||||
patchRequest(request._id, { description });
|
||||
setState({
|
||||
...state,
|
||||
request: updated,
|
||||
defaultPreviewMode: false,
|
||||
});
|
||||
};
|
||||
const updateName = (name: string) => {
|
||||
updateRequest({ name });
|
||||
const updated = { ...state.request, name } as Request;
|
||||
setState(state => ({
|
||||
...state,
|
||||
request: updated,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>
|
||||
Request Settings{' '}
|
||||
<span className="txt-sm selectable faint monospace">{request ? request._id : ''}</span>
|
||||
</ModalHeader>
|
||||
<ModalBody className="pad">
|
||||
<div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Name{' '}
|
||||
<span className="txt-sm faint italic">(also rename by double-clicking in sidebar)</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={request?.url || 'My Request'}
|
||||
defaultValue={request?.name}
|
||||
onChange={event => updateName(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} onHide={onHide}>
|
||||
<ModalHeader>
|
||||
Request Settings{' '}
|
||||
<span className="txt-sm selectable faint monospace">{request ? request._id : ''}</span>
|
||||
</ModalHeader>
|
||||
<ModalBody className="pad">
|
||||
<div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Name{' '}
|
||||
<span className="txt-sm faint italic">(also rename by double-clicking in sidebar)</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={request?.url || 'My Request'}
|
||||
defaultValue={request?.name}
|
||||
onChange={event => patchRequest(request._id, { name: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{request && isWebSocketRequest(request) && (
|
||||
<>
|
||||
<MarkdownEditor
|
||||
ref={editorRef}
|
||||
className="margin-top"
|
||||
defaultPreviewMode={defaultPreviewMode}
|
||||
placeholder="Write a description"
|
||||
defaultValue={request.description}
|
||||
onChange={updateDescription}
|
||||
/>
|
||||
<>
|
||||
<div className="pad-top pad-bottom">
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Send cookies automatically
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingSendCookies"
|
||||
checked={request.settingSendCookies}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Store cookies automatically
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingStoreCookies"
|
||||
checked={request.settingStoreCookies}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Follow redirects <span className="txt-sm faint italic">(overrides global setting)</span>
|
||||
<select
|
||||
defaultValue={request.settingFollowRedirects}
|
||||
name="settingFollowRedirects"
|
||||
onChange={toggleCheckBox}
|
||||
>
|
||||
<option value={'global'}>Use global setting</option>
|
||||
<option value={'off'}>Don't follow redirects</option>
|
||||
<option value={'on'}>Follow redirects</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
<hr />
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Move/Copy to Workspace
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Copy or move the current request to a new workspace. It will be placed at the root of
|
||||
the new workspace's folder structure.
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={activeWorkspaceIdToCopyTo || '__NULL__'}
|
||||
onChange={event => {
|
||||
const { value } = event.currentTarget;
|
||||
const workspaceId = value === '__NULL__' ? null : value;
|
||||
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
|
||||
}}
|
||||
>
|
||||
<option value="__NULL__">-- Select Workspace --</option>
|
||||
{workspacesForActiveProject.map(w => {
|
||||
if (workspaceId === w._id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<option key={w._id} value={w._id}>
|
||||
{w.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleCopyToWorkspace}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleMoveToWorkspace}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
{request && isGrpcRequest(request) && (
|
||||
<p className="faint italic">
|
||||
Are there any gRPC settings you expect to see? Create a{' '}
|
||||
<a href={'https://github.com/Kong/insomnia/issues/new/choose'}>feature request</a>!
|
||||
</p>
|
||||
)}
|
||||
{request && isRequest(request) && (
|
||||
<>
|
||||
<MarkdownEditor
|
||||
ref={editorRef}
|
||||
className="margin-top"
|
||||
defaultPreviewMode={defaultPreviewMode}
|
||||
placeholder="Write a description"
|
||||
defaultValue={request.description}
|
||||
onChange={updateDescription}
|
||||
/>
|
||||
<>
|
||||
<div className="pad-top pad-bottom">
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Send cookies automatically
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingSendCookies"
|
||||
checked={request.settingSendCookies}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Store cookies automatically
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingStoreCookies"
|
||||
checked={request.settingStoreCookies}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Automatically encode special characters in URL
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingEncodeUrl"
|
||||
checked={request.settingEncodeUrl}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Automatically encode special characters at send time (does not apply to query
|
||||
parameters editor)
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Skip rendering of request body
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingDisableRenderRequestBody"
|
||||
checked={request.settingDisableRenderRequestBody}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Disable rendering of environment variables and tags for the request body
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Rebuild path dot sequences
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
This instructs libcurl to squash sequences of "/../" or "/./" that may exist in the
|
||||
URL's path part and that is supposed to be removed according to RFC 3986 section
|
||||
5.2.4
|
||||
</HelpTooltip>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingRebuildPath"
|
||||
checked={request['settingRebuildPath']}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Follow redirects <span className="txt-sm faint italic">(overrides global setting)</span>
|
||||
<select
|
||||
defaultValue={request.settingFollowRedirects}
|
||||
name="settingFollowRedirects"
|
||||
onChange={async event => {
|
||||
const updated = await models.request.update(request, {
|
||||
[event.currentTarget.name]: event.currentTarget.value,
|
||||
});
|
||||
setState(state => ({ ...state, request: updated }));
|
||||
}}
|
||||
>
|
||||
<option value={'global'}>Use global setting</option>
|
||||
<option value={'off'}>Don't follow redirects</option>
|
||||
<option value={'on'}>Follow redirects</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
<hr />
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Move/Copy to Workspace
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Copy or move the current request to a new workspace. It will be placed at the root of
|
||||
the new workspace's folder structure.
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={activeWorkspaceIdToCopyTo || '__NULL__'}
|
||||
onChange={event => {
|
||||
const { value } = event.currentTarget;
|
||||
const workspaceId = value === '__NULL__' ? null : value;
|
||||
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
|
||||
}}
|
||||
>
|
||||
<option value="__NULL__">-- Select Workspace --</option>
|
||||
{workspacesForActiveProject.map(w => {
|
||||
if (workspaceId === w._id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<option key={w._id} value={w._id}>
|
||||
{w.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleCopyToWorkspace}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleMoveToWorkspace}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>)
|
||||
}
|
||||
</div>
|
||||
{request && isWebSocketRequest(request) && (
|
||||
<>
|
||||
<>
|
||||
{showDescription ? (
|
||||
<MarkdownEditor
|
||||
ref={editorRef}
|
||||
className="margin-top"
|
||||
defaultPreviewMode={defaultPreviewMode}
|
||||
placeholder="Write a description"
|
||||
defaultValue={request.description}
|
||||
onChange={updateDescription}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setState(state => ({ ...state, showDescription: true }))}
|
||||
className="btn btn--outlined btn--super-duper-compact"
|
||||
>
|
||||
Add Description
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
<div className="pad-top pad-bottom">
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Send cookies automatically
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingSendCookies"
|
||||
checked={request.settingSendCookies}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Store cookies automatically
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingStoreCookies"
|
||||
checked={request.settingStoreCookies}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Follow redirects <span className="txt-sm faint italic">(overrides global setting)</span>
|
||||
<select
|
||||
defaultValue={request.settingFollowRedirects}
|
||||
name="settingFollowRedirects"
|
||||
onChange={toggleCheckBox}
|
||||
>
|
||||
<option value={'global'}>Use global setting</option>
|
||||
<option value={'off'}>Don't follow redirects</option>
|
||||
<option value={'on'}>Follow redirects</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
<hr />
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Move/Copy to Workspace
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Copy or move the current request to a new workspace. It will be placed at the root of
|
||||
the new workspace's folder structure.
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={activeWorkspaceIdToCopyTo || '__NULL__'}
|
||||
onChange={event => {
|
||||
const { value } = event.currentTarget;
|
||||
const workspaceId = value === '__NULL__' ? null : value;
|
||||
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
|
||||
}}
|
||||
>
|
||||
<option value="__NULL__">-- Select Workspace --</option>
|
||||
{workspacesForActiveProject.map(w => {
|
||||
if (workspace && workspace._id === w._id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<option key={w._id} value={w._id}>
|
||||
{w.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleCopyToWorkspace}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleMoveToWorkspace}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
{request && isGrpcRequest(request) && (
|
||||
<p className="faint italic">
|
||||
Are there any gRPC settings you expect to see? Create a{' '}
|
||||
<a href={'https://github.com/Kong/insomnia/issues/new/choose'}>feature request</a>!
|
||||
</p>
|
||||
)}
|
||||
{request && isRequest(request) && (
|
||||
<>
|
||||
<>
|
||||
{showDescription ? (
|
||||
<MarkdownEditor
|
||||
ref={editorRef}
|
||||
className="margin-top"
|
||||
defaultPreviewMode={defaultPreviewMode}
|
||||
placeholder="Write a description"
|
||||
defaultValue={request.description}
|
||||
onChange={updateDescription}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setState(state => ({ ...state, showDescription: true }))}
|
||||
className="btn btn--outlined btn--super-duper-compact"
|
||||
>
|
||||
Add Description
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
<div className="pad-top pad-bottom">
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Send cookies automatically
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingSendCookies"
|
||||
checked={request.settingSendCookies}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Store cookies automatically
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingStoreCookies"
|
||||
checked={request.settingStoreCookies}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Automatically encode special characters in URL
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingEncodeUrl"
|
||||
checked={request.settingEncodeUrl}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Automatically encode special characters at send time (does not apply to query
|
||||
parameters editor)
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Skip rendering of request body
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingDisableRenderRequestBody"
|
||||
checked={request.settingDisableRenderRequestBody}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Disable rendering of environment variables and tags for the request body
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Rebuild path dot sequences
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
This instructs libcurl to squash sequences of "/../" or "/./" that may exist in the
|
||||
URL's path part and that is supposed to be removed according to RFC 3986 section
|
||||
5.2.4
|
||||
</HelpTooltip>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="settingRebuildPath"
|
||||
checked={request['settingRebuildPath']}
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Follow redirects <span className="txt-sm faint italic">(overrides global setting)</span>
|
||||
<select
|
||||
defaultValue={request.settingFollowRedirects}
|
||||
name="settingFollowRedirects"
|
||||
onChange={async event => {
|
||||
const updated = await models.request.update(request, {
|
||||
[event.currentTarget.name]: event.currentTarget.value,
|
||||
});
|
||||
setState(state => ({ ...state, request: updated }));
|
||||
}}
|
||||
>
|
||||
<option value={'global'}>Use global setting</option>
|
||||
<option value={'off'}>Don't follow redirects</option>
|
||||
<option value={'on'}>Follow redirects</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
<hr />
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Move/Copy to Workspace
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Copy or move the current request to a new workspace. It will be placed at the root of
|
||||
the new workspace's folder structure.
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={activeWorkspaceIdToCopyTo || '__NULL__'}
|
||||
onChange={event => {
|
||||
const { value } = event.currentTarget;
|
||||
const workspaceId = value === '__NULL__' ? null : value;
|
||||
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
|
||||
}}
|
||||
>
|
||||
<option value="__NULL__">-- Select Workspace --</option>
|
||||
{workspacesForActiveProject.map(w => {
|
||||
if (workspace && workspace._id === w._id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<option key={w._id} value={w._id}>
|
||||
{w.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleCopyToWorkspace}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button
|
||||
disabled={!activeWorkspaceIdToCopyTo}
|
||||
className="btn btn--clicky"
|
||||
onClick={handleMoveToWorkspace}
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>)
|
||||
}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
|
||||
RequestSettingsModal.displayName = 'RequestSettingsModal';
|
||||
};
|
||||
|
||||
@@ -1,458 +0,0 @@
|
||||
import classnames from 'classnames';
|
||||
import React, { forwardRef, Fragment, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { METHOD_GRPC } from '../../../common/constants';
|
||||
import { fuzzyMatchAll } from '../../../common/misc';
|
||||
import * as models from '../../../models';
|
||||
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { Workspace } from '../../../models/workspace';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
|
||||
import { useRequestMetaPatcher } from '../../hooks/use-request';
|
||||
import { selectGrpcRequestMetas, selectRequestMetas, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { Highlight } from '../base/highlight';
|
||||
import { Modal, ModalHandle, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder';
|
||||
import { GrpcTag } from '../tags/grpc-tag';
|
||||
import { MethodTag } from '../tags/method-tag';
|
||||
import { WebSocketTag } from '../tags/websocket-tag';
|
||||
import { wrapToIndex } from './utils';
|
||||
interface State {
|
||||
searchString: string;
|
||||
workspacesForActiveProject: Workspace[];
|
||||
matchedRequests: (Request | WebSocketRequest | GrpcRequest)[];
|
||||
matchedWorkspaces: Workspace[];
|
||||
activeIndex: number;
|
||||
maxRequests: number;
|
||||
maxWorkspaces: number;
|
||||
disableInput: boolean;
|
||||
selectOnKeyup: boolean;
|
||||
hideNeverActiveRequests: boolean;
|
||||
isModalVisible: boolean;
|
||||
title: string | null;
|
||||
}
|
||||
interface RequestSwitcherModalOptions {
|
||||
maxRequests?: number;
|
||||
maxWorkspaces?: number;
|
||||
disableInput?: boolean;
|
||||
selectOnKeyup?: boolean;
|
||||
hideNeverActiveRequests?: boolean;
|
||||
title?: string;
|
||||
openDelay?: number;
|
||||
}
|
||||
export interface RequestSwitcherModalHandle {
|
||||
show: (options?: RequestSwitcherModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const RequestSwitcherModal = forwardRef<RequestSwitcherModalHandle, ModalProps>((_, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<State>({
|
||||
searchString: '',
|
||||
workspacesForActiveProject: [],
|
||||
matchedRequests: [],
|
||||
matchedWorkspaces: [],
|
||||
activeIndex: -1,
|
||||
maxRequests: 20,
|
||||
maxWorkspaces: 20,
|
||||
disableInput: false,
|
||||
selectOnKeyup: false,
|
||||
hideNeverActiveRequests: false,
|
||||
isModalVisible: true,
|
||||
title: null,
|
||||
});
|
||||
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any> | undefined;
|
||||
const { activeRequest } = requestData || {};
|
||||
const {
|
||||
activeWorkspace: workspace,
|
||||
activeWorkspaceMeta,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
|
||||
const requestMetas = useSelector(selectRequestMetas);
|
||||
const grpcRequestMetas = useSelector(selectGrpcRequestMetas);
|
||||
const workspaceRequestsAndRequestGroups = useSelector(selectWorkspaceRequestsAndRequestGroups);
|
||||
const patchRequestMeta = useRequestMetaPatcher();
|
||||
/** Return array of path segments for given folders */
|
||||
const pathSegments = useCallback((requestOrRequestGroup: Request | WebSocketRequest | GrpcRequest | RequestGroup): string[] => {
|
||||
const folders = workspaceRequestsAndRequestGroups.filter(isRequestGroup)
|
||||
.filter(g => g._id === requestOrRequestGroup.parentId);
|
||||
const folderName = isRequestGroup(requestOrRequestGroup) ? `${requestOrRequestGroup.name}` : '';
|
||||
// It's the final parent
|
||||
if (folders.length === 0) {
|
||||
return [folderName];
|
||||
}
|
||||
// Still has more parents
|
||||
if (folderName) {
|
||||
return [...pathSegments(folders[0]), folderName];
|
||||
}
|
||||
// It's the child
|
||||
return pathSegments(folders[0]);
|
||||
}, [workspaceRequestsAndRequestGroups]);
|
||||
const getLastActiveRequestMap = useCallback(() => {
|
||||
// requestIds: lastActive datetime
|
||||
const lastActiveMap: Record<string, number> = {};
|
||||
|
||||
for (const meta of requestMetas) {
|
||||
lastActiveMap[meta.parentId] = meta.lastActive;
|
||||
}
|
||||
for (const meta of grpcRequestMetas) {
|
||||
lastActiveMap[meta.parentId] = meta.lastActive;
|
||||
}
|
||||
return lastActiveMap;
|
||||
}, [grpcRequestMetas, requestMetas]);
|
||||
|
||||
const isMatch = useCallback((request: Request | WebSocketRequest | GrpcRequest, searchStrings: string): number | null => {
|
||||
if (request._id === searchStrings) {
|
||||
return Infinity;
|
||||
}
|
||||
// name
|
||||
const searchIndexes = [request.name];
|
||||
// url
|
||||
isGrpcRequest(request) ? searchIndexes.push(request.url + request.protoMethodName)
|
||||
: searchIndexes.push(joinUrlAndQueryString(request.url, buildQueryStringFromParams(request.parameters)));
|
||||
// http method
|
||||
const method = isRequest(request) && request.method;
|
||||
method && searchIndexes.push(method);
|
||||
isGrpcRequest(request) && searchIndexes.push(METHOD_GRPC);
|
||||
// path segments
|
||||
searchIndexes.push(pathSegments(request).join('/'));
|
||||
const match = fuzzyMatchAll(
|
||||
searchStrings,
|
||||
searchIndexes,
|
||||
{ splitSpace: true },
|
||||
);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return match.score;
|
||||
}, [pathSegments]);
|
||||
const handleChangeValue = useCallback((searchString: string) => {
|
||||
const { maxRequests, maxWorkspaces, hideNeverActiveRequests } = state;
|
||||
const lastActiveMap = getLastActiveRequestMap();
|
||||
|
||||
// OPTIMIZATION: This only filters if we have a filter
|
||||
let matchedRequests = (workspaceRequestsAndRequestGroups
|
||||
.filter(child => isRequest(child) || isWebSocketRequest(child) || isGrpcRequest(child)) as (Request | WebSocketRequest | GrpcRequest)[])
|
||||
.sort((a, b) => {
|
||||
const aLA = lastActiveMap[a._id] || 0;
|
||||
const bLA = lastActiveMap[b._id] || 0;
|
||||
// If lastActive same, go by name
|
||||
if (aLA === bLA) {
|
||||
return a.name > b.name ? 1 : -1;
|
||||
}
|
||||
return bLA - aLA;
|
||||
});
|
||||
|
||||
if (hideNeverActiveRequests) {
|
||||
matchedRequests = matchedRequests.filter(r => lastActiveMap[r._id]);
|
||||
}
|
||||
|
||||
if (searchString) {
|
||||
matchedRequests = matchedRequests
|
||||
.map(r => ({
|
||||
request: r,
|
||||
score: isMatch(r, searchString),
|
||||
}))
|
||||
.filter(v => v.score !== null)
|
||||
.sort((a, b) => Number(a.score || -Infinity) - Number(b.score || -Infinity))
|
||||
.map(v => v.request);
|
||||
}
|
||||
|
||||
const matchedWorkspaces = workspacesForActiveProject
|
||||
.filter(w => w._id !== workspace._id)
|
||||
.filter(w => {
|
||||
const name = w.name.toLowerCase();
|
||||
const toMatch = searchString.toLowerCase();
|
||||
return name.indexOf(toMatch) !== -1;
|
||||
});
|
||||
// Make sure we select the first item but we don't want to select the currently active
|
||||
// one because that wouldn't make any sense.
|
||||
const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
|
||||
const indexOfFirstNonActiveRequest = matchedRequests.findIndex(r => r._id !== activeRequestId);
|
||||
setState(state => ({
|
||||
...state,
|
||||
searchString,
|
||||
activeIndex: indexOfFirstNonActiveRequest >= 0 ? indexOfFirstNonActiveRequest : 0,
|
||||
matchedRequests: matchedRequests.slice(0, maxRequests),
|
||||
matchedWorkspaces: matchedWorkspaces.slice(0, maxWorkspaces),
|
||||
}));
|
||||
}, [state, getLastActiveRequestMap, workspaceRequestsAndRequestGroups, workspacesForActiveProject, activeRequest, isMatch, workspace._id]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: options => {
|
||||
if (modalRef.current?.isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(state => ({
|
||||
...state,
|
||||
maxRequests: options?.maxRequests ?? 20,
|
||||
maxWorkspaces: options?.maxWorkspaces ?? 20,
|
||||
disableInput: !!options?.disableInput,
|
||||
hideNeverActiveRequests: !!options?.hideNeverActiveRequests,
|
||||
selectOnKeyup: !!options?.selectOnKeyup,
|
||||
title: options?.title || null,
|
||||
isModalVisible: true,
|
||||
}));
|
||||
handleChangeValue('');
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), [handleChangeValue]);
|
||||
|
||||
const activateWorkspaceAndHide = useCallback((workspace: Workspace) => {
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
console.log(`[app] Activating workspace "${workspace.name}"`);
|
||||
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}`);
|
||||
modalRef.current?.hide();
|
||||
}, [navigate, organizationId, projectId]);
|
||||
|
||||
const activateRequestAndHide = useCallback((request?: Request | WebSocketRequest | GrpcRequest) => {
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: request._id });
|
||||
patchRequestMeta(request._id, { lastActive: Date.now() });
|
||||
modalRef.current?.hide();
|
||||
}, [activeWorkspaceMeta, patchRequestMeta]);
|
||||
|
||||
const activateCurrentIndex = useCallback(async () => {
|
||||
const { activeIndex, matchedRequests, matchedWorkspaces, searchString } = state;
|
||||
if (activeIndex < matchedRequests.length) {
|
||||
// Activate the request if there is one
|
||||
const request = matchedRequests[activeIndex];
|
||||
activateRequestAndHide(request);
|
||||
return;
|
||||
}
|
||||
if (activeIndex < matchedRequests.length + matchedWorkspaces.length) {
|
||||
// Activate the workspace if there is one
|
||||
const index = activeIndex - matchedRequests.length;
|
||||
const workspace = matchedWorkspaces[index];
|
||||
if (workspace) {
|
||||
activateWorkspaceAndHide(workspace);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (searchString) {
|
||||
// Create request if no match
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the request if nothing matched
|
||||
const request = await models.request.create({
|
||||
parentId: activeRequest ? activeRequest.parentId : workspace._id,
|
||||
name: state.searchString,
|
||||
});
|
||||
|
||||
activateRequestAndHide(request);
|
||||
}
|
||||
}, [activateRequestAndHide, activateWorkspaceAndHide, activeRequest, state, workspace]);
|
||||
|
||||
const handleInputKeydown = createKeybindingsHandler({
|
||||
'ArrowUp': e => {
|
||||
e.preventDefault();
|
||||
setState(state => ({
|
||||
...state,
|
||||
activeIndex: wrapToIndex(state.activeIndex - 1, state.matchedRequests.length + state.matchedWorkspaces.length),
|
||||
}));
|
||||
},
|
||||
'Shift+Tab': e => {
|
||||
e.preventDefault();
|
||||
setState(state => ({
|
||||
...state,
|
||||
activeIndex: wrapToIndex(state.activeIndex - 1, state.matchedRequests.length + state.matchedWorkspaces.length),
|
||||
}));
|
||||
},
|
||||
'ArrowDown': e => {
|
||||
e.preventDefault();
|
||||
setState(state => ({
|
||||
...state,
|
||||
activeIndex: wrapToIndex(state.activeIndex + 1, state.matchedRequests.length + state.matchedWorkspaces.length),
|
||||
}));
|
||||
},
|
||||
'Tab': e => {
|
||||
e.preventDefault();
|
||||
setState(state => ({
|
||||
...state,
|
||||
activeIndex: wrapToIndex(state.activeIndex + 1, state.matchedRequests.length + state.matchedWorkspaces.length),
|
||||
}));
|
||||
},
|
||||
'Enter': e => {
|
||||
e.preventDefault();
|
||||
activateCurrentIndex();
|
||||
},
|
||||
});
|
||||
|
||||
useDocBodyKeyboardShortcuts({
|
||||
request_showRecent: () => {
|
||||
if (state.isModalVisible) {
|
||||
setState(state => ({
|
||||
...state,
|
||||
activeIndex: wrapToIndex(state.activeIndex + 1, state.matchedRequests.length + state.matchedWorkspaces.length),
|
||||
}));
|
||||
}
|
||||
},
|
||||
request_showRecentPrevious: () => {
|
||||
if (state.isModalVisible) {
|
||||
setState(state => ({
|
||||
...state,
|
||||
activeIndex: wrapToIndex(state.activeIndex - 1, state.matchedRequests.length + state.matchedWorkspaces.length),
|
||||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyup = async (event: KeyboardEvent) => {
|
||||
// Handle selection if unpresses all modifier keys. Ideally this would trigger once
|
||||
// the user unpresses the hotkey that triggered this modal but we currently do not
|
||||
// have the facilities to do that.
|
||||
const isMetaKeyDown = event.ctrlKey || event.shiftKey || event.metaKey || event.altKey;
|
||||
|
||||
if (state.selectOnKeyup && modalRef.current?.isOpen() && !isMetaKeyDown) {
|
||||
await activateCurrentIndex();
|
||||
modalRef.current?.hide();
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener('keyup', handleKeyup);
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener('keyup', handleKeyup);
|
||||
};
|
||||
}, [activateCurrentIndex, state.selectOnKeyup]);
|
||||
|
||||
const {
|
||||
searchString,
|
||||
activeIndex,
|
||||
matchedRequests,
|
||||
matchedWorkspaces,
|
||||
disableInput,
|
||||
title,
|
||||
isModalVisible,
|
||||
} = state;
|
||||
const requestGroups = workspaceRequestsAndRequestGroups.filter(isRequestGroup);
|
||||
return (
|
||||
<Modal
|
||||
ref={modalRef}
|
||||
className={isModalVisible ? '' : 'hide'}
|
||||
>
|
||||
<ModalHeader hideCloseButton>
|
||||
{title || (
|
||||
<Fragment>
|
||||
<div className="pull-right txt-sm pad-right tall">
|
||||
<span className="vertically-center">
|
||||
<div>
|
||||
<span className="monospace">tab</span> or
|
||||
<span className="monospace">↑↓</span> to navigate
|
||||
<span className="monospace">↵</span> to select
|
||||
<span className="monospace">esc</span> to dismiss
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div>Quick Switch</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody className="request-switcher">
|
||||
{!disableInput && (
|
||||
<div className="pad" onKeyDown={handleInputKeydown}>
|
||||
<div className="form-control form-control--outlined no-margin">
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
placeholder="Filter by name or folder"
|
||||
value={searchString}
|
||||
onChange={event => handleChangeValue(event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ul>
|
||||
{matchedRequests.map((r: Request | WebSocketRequest | GrpcRequest, i) => {
|
||||
const requestGroup = requestGroups.find(rg => rg._id === r.parentId);
|
||||
const buttonClasses = classnames(
|
||||
'btn btn--expandable-small wide text-left pad-bottom',
|
||||
{
|
||||
focus: activeIndex === i,
|
||||
},
|
||||
);
|
||||
return (
|
||||
<li key={r._id}>
|
||||
<button onClick={() => activateRequestAndHide(r)} className={buttonClasses}>
|
||||
<div>
|
||||
{requestGroup ? (
|
||||
<div className="pull-right faint italic">
|
||||
<Highlight search={searchString} text={pathSegments(r).join(' / ')} />
|
||||
|
||||
<i className="fa fa-folder-o" />
|
||||
</div>
|
||||
) : null}
|
||||
<Highlight search={searchString} text={r.name} />
|
||||
</div>
|
||||
<div className="margin-left-xs faint">
|
||||
{isRequest(r) ? <MethodTag method={r.method} /> : null}
|
||||
{isGrpcRequest(r) ? <GrpcTag /> : null}
|
||||
{isWebSocketRequest(r) ? <WebSocketTag /> : null}
|
||||
{<Highlight search={searchString} text={isGrpcRequest(r) ? r.url + r.protoMethodName : r.url} />}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
{matchedRequests.length > 0 && matchedWorkspaces.length > 0 && (
|
||||
<li className="pad-left pad-right">
|
||||
<hr />
|
||||
</li>
|
||||
)}
|
||||
|
||||
{matchedWorkspaces.map((w, i) => {
|
||||
const buttonClasses = classnames('btn btn--super-compact wide text-left', {
|
||||
focus: activeIndex - matchedRequests.length === i,
|
||||
});
|
||||
return (
|
||||
<li key={w._id}>
|
||||
<button onClick={() => activateWorkspaceAndHide(w)} className={buttonClasses}>
|
||||
<i className="fa fa-random" />
|
||||
Switch to <strong>{w.name}</strong>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{searchString && matchedRequests.length === 0 && matchedWorkspaces.length === 0 && (
|
||||
<div className="text-center pad-bottom">
|
||||
<p>
|
||||
No matches found for <strong>{searchString}</strong>
|
||||
</p>
|
||||
|
||||
{workspace ? <button
|
||||
className="btn btn--outlined btn--compact"
|
||||
disabled={!searchString}
|
||||
onClick={activateCurrentIndex}
|
||||
>
|
||||
Create a request named {searchString}
|
||||
</button> : null}
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
RequestSwitcherModal.displayName = 'RequestSwitcherModal';
|
||||
@@ -1,5 +1,6 @@
|
||||
import classnames from 'classnames';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { database as db, Operation } from '../../../common/database';
|
||||
@@ -30,7 +31,7 @@ export interface SyncBranchesModalHandle {
|
||||
show: (options: SyncBranchesModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const SyncBranchesModal = forwardRef<SyncBranchesModalHandle, Props>(({ vcs }, ref) => {
|
||||
export const SyncBranchesModal = ({ vcs, onHide }: Props) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<State>({
|
||||
error: '',
|
||||
@@ -69,17 +70,11 @@ export const SyncBranchesModal = forwardRef<SyncBranchesModalHandle, Props>(({ v
|
||||
}));
|
||||
}
|
||||
}, [vcs]);
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => modalRef.current?.hide(),
|
||||
show: ({ onHide }) => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
onHide,
|
||||
}));
|
||||
refreshState();
|
||||
modalRef.current?.show({ onHide });
|
||||
},
|
||||
}), [refreshState]);
|
||||
refreshState();
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
const syncItems = useSelector(selectSyncItems);
|
||||
async function handleCheckout(branch: string) {
|
||||
try {
|
||||
@@ -160,138 +155,140 @@ export const SyncBranchesModal = forwardRef<SyncBranchesModalHandle, Props>(({ v
|
||||
const { branches, remoteBranches, currentBranch, newBranchName, error } = state;
|
||||
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>Branches</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
<button className="pull-right icon" onClick={() => setState(state => ({ ...state, error: '' }))}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
New Branch Name
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => setState(state => ({ ...state, newBranchName: event.target.value }))}
|
||||
placeholder="testing-branch"
|
||||
value={newBranchName}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button type="submit" className="btn btn--clicky" disabled={!newBranchName}>
|
||||
Create
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} onHide={onHide}>
|
||||
<ModalHeader>Branches</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
<button className="pull-right icon" onClick={() => setState(state => ({ ...state, error: '' }))}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
New Branch Name
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => setState(state => ({ ...state, newBranchName: event.target.value }))}
|
||||
placeholder="testing-branch"
|
||||
value={newBranchName}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button type="submit" className="btn btn--clicky" disabled={!newBranchName}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
<div className="pad-top">
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Branches</th>
|
||||
<th className="text-right"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{branches.map(name => (
|
||||
<tr key={name} className="table--no-outline-row">
|
||||
<td>
|
||||
<span
|
||||
className={classnames({
|
||||
bold: name === currentBranch,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{name === currentBranch ? (
|
||||
<span className="txt-sm space-left">(current)</span>
|
||||
) : null}
|
||||
{name === 'master' && <i className="fa fa-lock space-left faint" />}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Merged"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => handleMerge(name)}
|
||||
>
|
||||
Merge
|
||||
</PromptButton>
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
disabled={name === currentBranch || name === 'master'}
|
||||
onClick={() => handleDelete(name)}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
<button
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => handleCheckout(name)}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{remoteBranches.length > 0 && (
|
||||
<div className="pad-top">
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Remote Branches</th>
|
||||
<th className="text-left">Branches</th>
|
||||
<th className="text-right"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{remoteBranches.map(name => (
|
||||
{branches.map(name => (
|
||||
<tr key={name} className="table--no-outline-row">
|
||||
<td>
|
||||
{name}
|
||||
<span
|
||||
className={classnames({
|
||||
bold: name === currentBranch,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{name === currentBranch ? (
|
||||
<span className="txt-sm space-left">(current)</span>
|
||||
) : null}
|
||||
{name === 'master' && <i className="fa fa-lock space-left faint" />}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{name !== 'master' && (
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => handleRemoteDelete(name)}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
)}
|
||||
<SyncPullButton
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
branch={name}
|
||||
onPull={refreshState}
|
||||
doneMessage="Merged"
|
||||
disabled={name === currentBranch}
|
||||
vcs={vcs}
|
||||
onClick={() => handleMerge(name)}
|
||||
>
|
||||
Fetch
|
||||
</SyncPullButton>
|
||||
Merge
|
||||
</PromptButton>
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
disabled={name === currentBranch || name === 'master'}
|
||||
onClick={() => handleDelete(name)}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
<button
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => handleCheckout(name)}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
</Modal >
|
||||
|
||||
{remoteBranches.length > 0 && (
|
||||
<div className="pad-top">
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Remote Branches</th>
|
||||
<th className="text-right"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{remoteBranches.map(name => (
|
||||
<tr key={name} className="table--no-outline-row">
|
||||
<td>
|
||||
{name}
|
||||
{name === 'master' && <i className="fa fa-lock space-left faint" />}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{name !== 'master' && (
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => handleRemoteDelete(name)}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
)}
|
||||
<SyncPullButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
branch={name}
|
||||
onPull={refreshState}
|
||||
disabled={name === currentBranch}
|
||||
vcs={vcs}
|
||||
>
|
||||
Fetch
|
||||
</SyncPullButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
};
|
||||
SyncBranchesModal.displayName = 'SyncBranchesModal';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { strings } from '../../../common/strings';
|
||||
@@ -16,16 +17,9 @@ type Props = ModalProps & {
|
||||
interface State {
|
||||
error?: string;
|
||||
workspaceName: string;
|
||||
onHide?: () => void;
|
||||
}
|
||||
export interface SyncDeleteModalOptions {
|
||||
onHide?: () => void;
|
||||
}
|
||||
export interface SyncDeleteModalHandle {
|
||||
show: (options: SyncDeleteModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }, ref) => {
|
||||
|
||||
export const SyncDeleteModal = ({ vcs, onHide }: Props) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<State>({
|
||||
error: '',
|
||||
@@ -35,17 +29,9 @@ export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }
|
||||
activeWorkspace,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => modalRef.current?.hide(),
|
||||
show: ({ onHide }) => {
|
||||
setState({
|
||||
error: '',
|
||||
workspaceName: '',
|
||||
onHide,
|
||||
});
|
||||
modalRef.current?.show({ onHide });
|
||||
},
|
||||
}), []);
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
const onSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
@@ -56,7 +42,7 @@ export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }
|
||||
resourceType: strings.collection.singular.toLowerCase(),
|
||||
});
|
||||
modalRef.current?.hide();
|
||||
state.onHide?.();
|
||||
onHide?.();
|
||||
} catch (err) {
|
||||
setState(state => ({
|
||||
...state,
|
||||
@@ -67,29 +53,30 @@ export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }
|
||||
const { error, workspaceName } = state;
|
||||
|
||||
return (
|
||||
<Modal ref={modalRef} skinny>
|
||||
<ModalHeader>Delete {strings.collection.singular}</ModalHeader>
|
||||
<ModalBody className="wide pad-left pad-right text-center" noScroll>
|
||||
{error && <p className="notice error margin-bottom-sm no-margin-top">{error}</p>}
|
||||
<p className="selectable">
|
||||
This will permanently delete the {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>}{' '}
|
||||
{strings.collection.singular.toLowerCase()} remotely.
|
||||
</p>
|
||||
<p className="selectable">Please type {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>} to confirm.</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-control form-control--outlined">
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => setState(state => ({ ...state, workspaceName: event.target.value }))}
|
||||
value={workspaceName}
|
||||
/>
|
||||
<Button bg="danger" disabled={workspaceName !== activeWorkspace?.name}>
|
||||
Delete {strings.collection.singular}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} skinny onHide={onHide}>
|
||||
<ModalHeader>Delete {strings.collection.singular}</ModalHeader>
|
||||
<ModalBody className="wide pad-left pad-right text-center" noScroll>
|
||||
{error && <p className="notice error margin-bottom-sm no-margin-top">{error}</p>}
|
||||
<p className="selectable">
|
||||
This will permanently delete the {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>}{' '}
|
||||
{strings.collection.singular.toLowerCase()} remotely.
|
||||
</p>
|
||||
<p className="selectable">Please type {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>} to confirm.</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-control form-control--outlined">
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => setState(state => ({ ...state, workspaceName: event.target.value }))}
|
||||
value={workspaceName}
|
||||
/>
|
||||
<Button bg="danger" disabled={workspaceName !== activeWorkspace?.name}>
|
||||
Delete {strings.collection.singular}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
SyncDeleteModal.displayName = 'SyncDeleteModal';
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classnames from 'classnames';
|
||||
import React, { FC, forwardRef, Fragment, useImperativeHandle, useRef } from 'react';
|
||||
import { ListDropTargetDelegate, ListKeyboardDelegate, mergeProps, useDraggableCollection, useDraggableItem, useDropIndicator, useDroppableCollection, useDroppableItem, useFocusRing, useListBox, useOption } from 'react-aria';
|
||||
import React, { FC, Fragment, useEffect, useRef } from 'react';
|
||||
import { ListDropTargetDelegate, ListKeyboardDelegate, mergeProps, OverlayContainer, useDraggableCollection, useDraggableItem, useDropIndicator, useDroppableCollection, useDroppableItem, useFocusRing, useListBox, useOption } from 'react-aria';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { DraggableCollectionState, DroppableCollectionState, Item, ListState, useDraggableCollectionState, useDroppableCollectionState, useListState } from 'react-stately';
|
||||
|
||||
@@ -226,11 +226,7 @@ const ReorderableListBox = props => {
|
||||
);
|
||||
};
|
||||
|
||||
export interface WorkspaceEnvironmentsEditModalHandle {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEditModalHandle, ModalProps>((props, ref) => {
|
||||
export const WorkspaceEnvironmentsEditModal = (props: ModalProps) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const routeData = useRouteLoaderData(
|
||||
':workspaceId'
|
||||
@@ -244,15 +240,9 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
|
||||
const updateEnvironmentFetcher = useFetcher();
|
||||
const setActiveEnvironmentFetcher = useFetcher();
|
||||
const duplicateEnvironmentFetcher = useFetcher();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async () => {
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), []);
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
if (!routeData) {
|
||||
return null;
|
||||
@@ -272,10 +262,10 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
|
||||
setActiveEnvironmentFetcher.submit({
|
||||
environmentId,
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
|
||||
});
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,10 +273,10 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
|
||||
deleteEnvironmentFetcher.submit({
|
||||
environmentId,
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/delete`,
|
||||
});
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/delete`,
|
||||
});
|
||||
}
|
||||
|
||||
const updateEnvironment = async (environmentId: string, patch: Partial<Environment>) => {
|
||||
@@ -294,11 +284,11 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
|
||||
patch,
|
||||
environmentId,
|
||||
},
|
||||
{
|
||||
encType: 'application/json',
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`,
|
||||
});
|
||||
{
|
||||
encType: 'application/json',
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`,
|
||||
});
|
||||
};
|
||||
|
||||
function onReorder(e: any) {
|
||||
@@ -319,233 +309,234 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal ref={modalRef} wide tall {...props}>
|
||||
<ModalHeader>Manage Environments</ModalHeader>
|
||||
<ModalBody noScroll className="env-modal">
|
||||
<div className="env-modal__sidebar">
|
||||
<div
|
||||
className={classnames('env-modal__sidebar-root-item', {
|
||||
'env-modal__sidebar-item--active': activeEnvironment._id === baseEnvironment._id,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (environmentEditorRef.current?.isValid() && activeEnvironment._id !== baseEnvironment._id) {
|
||||
setActiveEnvironmentFetcher.submit({
|
||||
environmentId: baseEnvironment._id,
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} wide tall onHide={props.onHide}>
|
||||
<ModalHeader>Manage Environments</ModalHeader>
|
||||
<ModalBody noScroll className="env-modal">
|
||||
<div className="env-modal__sidebar">
|
||||
<div
|
||||
className={classnames('env-modal__sidebar-root-item', {
|
||||
'env-modal__sidebar-item--active': activeEnvironment._id === baseEnvironment._id,
|
||||
})}
|
||||
>
|
||||
{ROOT_ENVIRONMENT_NAME}
|
||||
<HelpTooltip className="space-left">
|
||||
The variables in this environment are always available, regardless of which
|
||||
sub-environment is active. Useful for storing default or fallback values.
|
||||
</HelpTooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div className="pad env-modal__sidebar-heading">
|
||||
<h3 className="no-margin">Sub Environments</h3>
|
||||
<Dropdown
|
||||
aria-label='Create Environment Dropdown'
|
||||
triggerButton={
|
||||
<DropdownButton
|
||||
data-testid='CreateEnvironmentDropdown'
|
||||
>
|
||||
<i className="fa fa-plus-circle" />
|
||||
<i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label='Environment'>
|
||||
<ItemContent
|
||||
icon="eye"
|
||||
label="Environment"
|
||||
onClick={async () => {
|
||||
createEnvironmentFetcher.submit({
|
||||
isPrivate: false,
|
||||
<button
|
||||
onClick={() => {
|
||||
if (environmentEditorRef.current?.isValid() && activeEnvironment._id !== baseEnvironment._id) {
|
||||
setActiveEnvironmentFetcher.submit({
|
||||
environmentId: baseEnvironment._id,
|
||||
},
|
||||
{
|
||||
encType: 'application/json',
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Private Environment'>
|
||||
<ItemContent
|
||||
icon="eye-slash"
|
||||
label="Private Environment"
|
||||
onClick={async () => {
|
||||
createEnvironmentFetcher.submit({
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
encType: 'application/json',
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<ReorderableListBox
|
||||
items={subEnvironments}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onReorder={onReorder}
|
||||
selectionMode="multiple"
|
||||
selectionBehavior="replace"
|
||||
aria-label="list of subenvironments"
|
||||
>
|
||||
{(environment: any) =>
|
||||
<Item key={environment._id}>
|
||||
{environment.name}
|
||||
</Item>
|
||||
}
|
||||
</ReorderableListBox>
|
||||
</div>
|
||||
<div className="env-modal__main">
|
||||
<div className="env-modal__main__header">
|
||||
<h1>
|
||||
{baseEnvironment._id === activeEnvironment._id ? (
|
||||
ROOT_ENVIRONMENT_NAME
|
||||
) : (
|
||||
<Editable
|
||||
singleClick
|
||||
className="wide"
|
||||
onSubmit={name => {
|
||||
if (activeEnvironment._id && name) {
|
||||
updateEnvironment(activeEnvironment._id, { name });
|
||||
}
|
||||
}}
|
||||
value={activeEnvironment.name}
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
{baseEnvironment._id !== activeEnvironment._id ? (
|
||||
<Fragment>
|
||||
<input
|
||||
className="hidden"
|
||||
type="color"
|
||||
ref={inputRef}
|
||||
onChange={event => updateEnvironment(activeEnvironment._id, { color: event.target.value })}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
aria-label='Environment Color Dropdown'
|
||||
className="space-right"
|
||||
triggerButton={
|
||||
<DropdownButton
|
||||
className="btn btn--clicky"
|
||||
disableHoverBehavior={false}
|
||||
>
|
||||
{activeEnvironment.color && (
|
||||
<i
|
||||
className="fa fa-circle space-right"
|
||||
style={{
|
||||
color: activeEnvironment.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
Color <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label={activeEnvironment.color ? 'Change Color' : 'Assign Color'}>
|
||||
<ItemContent
|
||||
icon="circle"
|
||||
label={activeEnvironment.color ? 'Change Color' : 'Assign Color'}
|
||||
iconStyle={{
|
||||
...(activeEnvironment.color ? { color: activeEnvironment.color } : {}),
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!activeEnvironment.color) {
|
||||
// TODO: fix magic-number. Currently this is the `surprise` background color for the default theme,
|
||||
// but we should be grabbing the actual value from the user's actual theme instead.
|
||||
updateEnvironment(activeEnvironment._id, { color: '#7d69cb' });
|
||||
}
|
||||
inputRef.current?.click();
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem aria-label='Unset Color'>
|
||||
<ItemContent
|
||||
isDisabled={!activeEnvironment.color}
|
||||
icon="minus-circle"
|
||||
label="Unset Color"
|
||||
onClick={() => updateEnvironment(activeEnvironment._id, { color: null })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (activeEnvironment) {
|
||||
duplicateEnvironmentFetcher.submit({
|
||||
environmentId: activeEnvironment._id,
|
||||
}, {
|
||||
{
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/duplicate`,
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="btn btn--clicky space-right"
|
||||
>
|
||||
<i className="fa fa-copy" /> Duplicate
|
||||
</button>
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ROOT_ENVIRONMENT_NAME}
|
||||
<HelpTooltip className="space-left">
|
||||
The variables in this environment are always available, regardless of which
|
||||
sub-environment is active. Useful for storing default or fallback values.
|
||||
</HelpTooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div className="pad env-modal__sidebar-heading">
|
||||
<h3 className="no-margin">Sub Environments</h3>
|
||||
<Dropdown
|
||||
aria-label='Create Environment Dropdown'
|
||||
triggerButton={
|
||||
<DropdownButton
|
||||
data-testid='CreateEnvironmentDropdown'
|
||||
>
|
||||
<i className="fa fa-plus-circle" />
|
||||
<i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label='Environment'>
|
||||
<ItemContent
|
||||
icon="eye"
|
||||
label="Environment"
|
||||
onClick={async () => {
|
||||
createEnvironmentFetcher.submit({
|
||||
isPrivate: false,
|
||||
},
|
||||
{
|
||||
encType: 'application/json',
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Private Environment'>
|
||||
<ItemContent
|
||||
icon="eye-slash"
|
||||
label="Private Environment"
|
||||
onClick={async () => {
|
||||
createEnvironmentFetcher.submit({
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
encType: 'application/json',
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<ReorderableListBox
|
||||
items={subEnvironments}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onReorder={onReorder}
|
||||
selectionMode="multiple"
|
||||
selectionBehavior="replace"
|
||||
aria-label="list of subenvironments"
|
||||
>
|
||||
{(environment: any) =>
|
||||
<Item key={environment._id}>
|
||||
{environment.name}
|
||||
</Item>
|
||||
}
|
||||
</ReorderableListBox>
|
||||
</div>
|
||||
<div className="env-modal__main">
|
||||
<div className="env-modal__main__header">
|
||||
<h1>
|
||||
{baseEnvironment._id === activeEnvironment._id ? (
|
||||
ROOT_ENVIRONMENT_NAME
|
||||
) : (
|
||||
<Editable
|
||||
singleClick
|
||||
className="wide"
|
||||
onSubmit={name => {
|
||||
if (activeEnvironment._id && name) {
|
||||
updateEnvironment(activeEnvironment._id, { name });
|
||||
}
|
||||
}}
|
||||
value={activeEnvironment.name}
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
{activeEnvironment._id !== baseEnvironment._id && <PromptButton
|
||||
onClick={() => handleDeleteEnvironment(activeEnvironment._id)}
|
||||
className="btn btn--clicky"
|
||||
>
|
||||
<i className="fa fa-trash-o" />
|
||||
</PromptButton>}
|
||||
</Fragment>
|
||||
) : null}
|
||||
{baseEnvironment._id !== activeEnvironment._id ? (
|
||||
<Fragment>
|
||||
<input
|
||||
className="hidden"
|
||||
type="color"
|
||||
ref={inputRef}
|
||||
onChange={event => updateEnvironment(activeEnvironment._id, { color: event.target.value })}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
aria-label='Environment Color Dropdown'
|
||||
className="space-right"
|
||||
triggerButton={
|
||||
<DropdownButton
|
||||
className="btn btn--clicky"
|
||||
disableHoverBehavior={false}
|
||||
>
|
||||
{activeEnvironment.color && (
|
||||
<i
|
||||
className="fa fa-circle space-right"
|
||||
style={{
|
||||
color: activeEnvironment.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
Color <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label={activeEnvironment.color ? 'Change Color' : 'Assign Color'}>
|
||||
<ItemContent
|
||||
icon="circle"
|
||||
label={activeEnvironment.color ? 'Change Color' : 'Assign Color'}
|
||||
iconStyle={{
|
||||
...(activeEnvironment.color ? { color: activeEnvironment.color } : {}),
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!activeEnvironment.color) {
|
||||
// TODO: fix magic-number. Currently this is the `surprise` background color for the default theme,
|
||||
// but we should be grabbing the actual value from the user's actual theme instead.
|
||||
updateEnvironment(activeEnvironment._id, { color: '#7d69cb' });
|
||||
}
|
||||
inputRef.current?.click();
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem aria-label='Unset Color'>
|
||||
<ItemContent
|
||||
isDisabled={!activeEnvironment.color}
|
||||
icon="minus-circle"
|
||||
label="Unset Color"
|
||||
onClick={() => updateEnvironment(activeEnvironment._id, { color: null })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (activeEnvironment) {
|
||||
duplicateEnvironmentFetcher.submit({
|
||||
environmentId: activeEnvironment._id,
|
||||
}, {
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/duplicate`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="btn btn--clicky space-right"
|
||||
>
|
||||
<i className="fa fa-copy" /> Duplicate
|
||||
</button>
|
||||
|
||||
{activeEnvironment._id !== baseEnvironment._id && <PromptButton
|
||||
onClick={() => handleDeleteEnvironment(activeEnvironment._id)}
|
||||
className="btn btn--clicky"
|
||||
>
|
||||
<i className="fa fa-trash-o" />
|
||||
</PromptButton>}
|
||||
</Fragment>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="env-modal__editor">
|
||||
<EnvironmentEditor
|
||||
ref={environmentEditorRef}
|
||||
key={`${activeEnvironment._id}`}
|
||||
environmentInfo={{
|
||||
object: activeEnvironment.data,
|
||||
propertyOrder: activeEnvironment.dataPropertyOrder,
|
||||
}}
|
||||
onBlur={() => {
|
||||
// Only save if it's valid
|
||||
if (!environmentEditorRef.current || !environmentEditorRef.current?.isValid()) {
|
||||
return;
|
||||
}
|
||||
const data = environmentEditorRef.current?.getValue();
|
||||
if (activeEnvironment && data) {
|
||||
updateEnvironment(activeEnvironment._id, {
|
||||
data: data.object,
|
||||
dataPropertyOrder: data.propertyOrder,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="env-modal__editor">
|
||||
<EnvironmentEditor
|
||||
ref={environmentEditorRef}
|
||||
key={`${activeEnvironment._id}`}
|
||||
environmentInfo={{
|
||||
object: activeEnvironment.data,
|
||||
propertyOrder: activeEnvironment.dataPropertyOrder,
|
||||
}}
|
||||
onBlur={() => {
|
||||
// Only save if it's valid
|
||||
if (!environmentEditorRef.current || !environmentEditorRef.current?.isValid()) {
|
||||
return;
|
||||
}
|
||||
const data = environmentEditorRef.current?.getValue();
|
||||
if (activeEnvironment && data) {
|
||||
updateEnvironment(activeEnvironment._id, {
|
||||
data: data.object,
|
||||
dataPropertyOrder: data.propertyOrder,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
* Environment data can be used for
|
||||
<Link href={docsTemplateTags}>Nunjucks Templating</Link> in your requests
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
* Environment data can be used for
|
||||
<Link href={docsTemplateTags}>Nunjucks Templating</Link> in your requests
|
||||
</div>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Close
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
<button className="btn" onClick={() => modalRef.current?.hide()}>
|
||||
Close
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
WorkspaceEnvironmentsEditModal.displayName = 'WorkspaceEnvironmentsEditModal';
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ export const WorkspaceSettingsModal = ({ workspace, workspaceMeta, clientCertifi
|
||||
const activeWorkspaceName = workspace.name;
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
|
||||
const workspaceFetcher = useFetcher();
|
||||
|
||||
@@ -9,12 +9,12 @@ import { generateId } from '../../../common/misc';
|
||||
import { getRenderContext, getRenderedGrpcRequest, getRenderedGrpcRequestMessage, render, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import { GrpcMethodType } from '../../../main/ipc/grpc';
|
||||
import * as models from '../../../models';
|
||||
import type { GrpcRequest, GrpcRequestHeader } from '../../../models/grpc-request';
|
||||
import type { GrpcRequestHeader } from '../../../models/grpc-request';
|
||||
import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { GrpcRequestState } from '../../routes/debug';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { GrpcRequestLoaderData } from '../../routes/request';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
|
||||
import { GrpcSendButton } from '../buttons/grpc-send-button';
|
||||
@@ -73,7 +73,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
setGrpcState,
|
||||
reloadRequests,
|
||||
}) => {
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<GrpcRequest, any>;
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as GrpcRequestLoaderData;
|
||||
|
||||
const [isProtoModalOpen, setIsProtoModalOpen] = useState(false);
|
||||
const { requestMessages, running, methods } = grpcState;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -6,11 +6,9 @@ import { getContentTypeFromHeaders } from '../../../common/constants';
|
||||
import { database } from '../../../common/database';
|
||||
import * as models from '../../../models';
|
||||
import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls';
|
||||
import { Request } from '../../../models/request';
|
||||
import { RequestMeta } from '../../../models/request-meta';
|
||||
import type { Settings } from '../../../models/settings';
|
||||
import { deconstructQueryStringToParams, extractQueryStringFromUrl } from '../../../utils/url/querystring';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { useRequestPatcher, useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
@@ -24,7 +22,6 @@ import { RequestHeadersEditor } from '../editors/request-headers-editor';
|
||||
import { RequestParametersEditor } from '../editors/request-parameters-editor';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
import { MarkdownPreview } from '../markdown-preview';
|
||||
import { showModal } from '../modals';
|
||||
import { RequestSettingsModal } from '../modals/request-settings-modal';
|
||||
import { RenderedQueryString } from '../rendered-query-string';
|
||||
import { RequestUrlBar, RequestUrlBarHandle } from '../request-url-bar';
|
||||
@@ -68,25 +65,11 @@ export const RequestPane: FC<Props> = ({
|
||||
settings,
|
||||
setLoading,
|
||||
}) => {
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const { workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const patchRequest = useRequestPatcher();
|
||||
|
||||
const handleEditDescription = useCallback((forceEditMode: boolean) => {
|
||||
showModal(RequestSettingsModal, { request: activeRequest, forceEditMode });
|
||||
}, [activeRequest]);
|
||||
|
||||
const handleEditDescriptionAdd = useCallback(() => {
|
||||
handleEditDescription(true);
|
||||
}, [handleEditDescription]);
|
||||
|
||||
const handleUpdateSettingsUseBulkHeaderEditor = useCallback(() => {
|
||||
models.settings.update(settings, { useBulkHeaderEditor: !settings.useBulkHeaderEditor });
|
||||
}, [settings]);
|
||||
|
||||
const handleUpdateSettingsUseBulkParametersEditor = useCallback(() => {
|
||||
models.settings.update(settings, { useBulkParametersEditor: !settings.useBulkParametersEditor });
|
||||
}, [settings]);
|
||||
const patchSettings = useSettingsPatcher();
|
||||
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
|
||||
|
||||
const handleImportQueryFromUrl = useCallback(() => {
|
||||
let query;
|
||||
@@ -202,7 +185,7 @@ export const RequestPane: FC<Props> = ({
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--compact"
|
||||
onClick={handleUpdateSettingsUseBulkParametersEditor}
|
||||
onClick={() => patchSettings({ useBulkParametersEditor: !settings.useBulkParametersEditor })}
|
||||
>
|
||||
{settings.useBulkParametersEditor ? 'Regular Edit' : 'Bulk Edit'}
|
||||
</button>
|
||||
@@ -222,7 +205,7 @@ export const RequestPane: FC<Props> = ({
|
||||
<TabPanelFooter>
|
||||
<button
|
||||
className="btn btn--compact"
|
||||
onClick={handleUpdateSettingsUseBulkHeaderEditor}
|
||||
onClick={() => patchSettings({ useBulkHeaderEditor: !settings.useBulkHeaderEditor })}
|
||||
>
|
||||
{settings.useBulkHeaderEditor ? 'Regular Edit' : 'Bulk Edit'}
|
||||
</button>
|
||||
@@ -246,8 +229,7 @@ export const RequestPane: FC<Props> = ({
|
||||
{activeRequest.description ? (
|
||||
<div>
|
||||
<div className="pull-right pad bg-default">
|
||||
{/* @ts-expect-error -- TSCONVERSION the click handler expects a boolean prop... */}
|
||||
<button className="btn btn--clicky" onClick={handleEditDescription}>
|
||||
<button className="btn btn--clicky" onClick={() => setIsRequestSettingsModalOpen(true)}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
@@ -274,10 +256,7 @@ export const RequestPane: FC<Props> = ({
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<button
|
||||
className="btn btn--clicky faint"
|
||||
onClick={handleEditDescriptionAdd}
|
||||
>
|
||||
<button className="btn btn--clicky faint" onClick={() => setIsRequestSettingsModalOpen(true)}>
|
||||
Add Description
|
||||
</button>
|
||||
</p>
|
||||
@@ -286,6 +265,12 @@ export const RequestPane: FC<Props> = ({
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
{isRequestSettingsModalOpen && (
|
||||
<RequestSettingsModal
|
||||
request={activeRequest}
|
||||
onHide={() => setIsRequestSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import fs from 'fs';
|
||||
import { extension as mimeExtension } from 'mime-types';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { PREVIEW_MODE_SOURCE } from '../../../common/constants';
|
||||
import { getSetCookieHeaders } from '../../../common/misc';
|
||||
import * as models from '../../../models';
|
||||
import type { Request } from '../../../models/request';
|
||||
import { RequestMeta } from '../../../models/request-meta';
|
||||
import type { Response } from '../../../models/response';
|
||||
import { cancelRequestById } from '../../../network/cancellation';
|
||||
import { jsonPrettify } from '../../../utils/prettify/json';
|
||||
import { useRequestMetaPatcher } from '../../hooks/use-request';
|
||||
import { selectActiveResponse } from '../../redux/selectors';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
|
||||
@@ -39,8 +34,7 @@ interface Props {
|
||||
export const ResponsePane: FC<Props> = ({
|
||||
runningRequests,
|
||||
}) => {
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
|
||||
const response = useSelector(selectActiveResponse) as Response | null;
|
||||
const { activeRequest, activeRequestMeta, activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const filterHistory = activeRequestMeta.responseFilterHistory || [];
|
||||
const filter = activeRequestMeta.responseFilter || '';
|
||||
const patchRequestMeta = useRequestMetaPatcher();
|
||||
@@ -49,10 +43,10 @@ export const ResponsePane: FC<Props> = ({
|
||||
} = useRouteLoaderData('root') as RootLoaderData;
|
||||
const previewMode = activeRequestMeta.previewMode || PREVIEW_MODE_SOURCE;
|
||||
const handleSetFilter = async (responseFilter: string) => {
|
||||
if (!response) {
|
||||
if (!activeResponse) {
|
||||
return;
|
||||
}
|
||||
const requestId = response.parentId;
|
||||
const requestId = activeResponse.parentId;
|
||||
await patchRequestMeta(requestId, { responseFilter });
|
||||
const meta = await models.requestMeta.getByParentId(requestId);
|
||||
if (!meta) {
|
||||
@@ -67,11 +61,11 @@ export const ResponsePane: FC<Props> = ({
|
||||
patchRequestMeta(requestId, { responseFilterHistory });
|
||||
};
|
||||
const handleGetResponseBody = useCallback(() => {
|
||||
if (!response) {
|
||||
if (!activeResponse) {
|
||||
return null;
|
||||
}
|
||||
return models.response.getBodyBuffer(response);
|
||||
}, [response]);
|
||||
return models.response.getBodyBuffer(activeResponse);
|
||||
}, [activeResponse]);
|
||||
const handleCopyResponseToClipboard = useCallback(async () => {
|
||||
const bodyBuffer = handleGetResponseBody();
|
||||
if (bodyBuffer) {
|
||||
@@ -79,12 +73,12 @@ export const ResponsePane: FC<Props> = ({
|
||||
}
|
||||
}, [handleGetResponseBody]);
|
||||
const handleDownloadResponseBody = useCallback(async (prettify: boolean) => {
|
||||
if (!response || !activeRequest) {
|
||||
if (!activeResponse || !activeRequest) {
|
||||
console.warn('Nothing to download');
|
||||
return;
|
||||
}
|
||||
|
||||
const { contentType } = response;
|
||||
const { contentType } = activeResponse;
|
||||
const extension = mimeExtension(contentType) || 'unknown';
|
||||
const { canceled, filePath: outputPath } = await window.dialog.showSaveDialog({
|
||||
title: 'Save Response Body',
|
||||
@@ -96,7 +90,7 @@ export const ResponsePane: FC<Props> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const readStream = models.response.getBodyStream(response);
|
||||
const readStream = models.response.getBodyStream(activeResponse);
|
||||
const dataBuffers: any[] = [];
|
||||
|
||||
if (readStream && outputPath && typeof readStream !== 'string') {
|
||||
@@ -123,14 +117,14 @@ export const ResponsePane: FC<Props> = ({
|
||||
to.end();
|
||||
});
|
||||
}
|
||||
}, [activeRequest, response]);
|
||||
}, [activeRequest, activeResponse]);
|
||||
|
||||
if (!activeRequest) {
|
||||
return <BlankPane type="response" />;
|
||||
}
|
||||
|
||||
// If there is no previous response, show placeholder for loading indicator
|
||||
if (!response) {
|
||||
if (!activeResponse) {
|
||||
return (
|
||||
<PlaceholderResponsePane>
|
||||
{runningRequests[activeRequest._id] && <ResponseTimer
|
||||
@@ -139,20 +133,19 @@ export const ResponsePane: FC<Props> = ({
|
||||
</PlaceholderResponsePane>
|
||||
);
|
||||
}
|
||||
const timeline = models.response.getTimeline(response);
|
||||
const cookieHeaders = getSetCookieHeaders(response.headers);
|
||||
const timeline = models.response.getTimeline(activeResponse);
|
||||
const cookieHeaders = getSetCookieHeaders(activeResponse.headers);
|
||||
return (
|
||||
<Pane type="response">
|
||||
{!response ? null : (
|
||||
{!activeResponse ? null : (
|
||||
<PaneHeader className="row-spaced">
|
||||
<div aria-atomic="true" aria-live="polite" className="no-wrap scrollable scrollable--no-bars pad-left">
|
||||
<StatusTag statusCode={response.statusCode} statusMessage={response.statusMessage} />
|
||||
<TimeTag milliseconds={response.elapsedTime} />
|
||||
<SizeTag bytesRead={response.bytesRead} bytesContent={response.bytesContent} />
|
||||
<StatusTag statusCode={activeResponse.statusCode} statusMessage={activeResponse.statusMessage} />
|
||||
<TimeTag milliseconds={activeResponse.elapsedTime} />
|
||||
<SizeTag bytesRead={activeResponse.bytesRead} bytesContent={activeResponse.bytesContent} />
|
||||
</div>
|
||||
<ResponseHistoryDropdown
|
||||
activeResponse={response}
|
||||
className="tall pane__header__right"
|
||||
activeResponse={activeResponse}
|
||||
/>
|
||||
</PaneHeader>
|
||||
)}
|
||||
@@ -167,21 +160,21 @@ export const ResponsePane: FC<Props> = ({
|
||||
}
|
||||
>
|
||||
<ResponseViewer
|
||||
key={response._id}
|
||||
bytes={Math.max(response.bytesContent, response.bytesRead)}
|
||||
contentType={response.contentType || ''}
|
||||
key={activeResponse._id}
|
||||
bytes={Math.max(activeResponse.bytesContent, activeResponse.bytesRead)}
|
||||
contentType={activeResponse.contentType || ''}
|
||||
disableHtmlPreviewJs={settings.disableHtmlPreviewJs}
|
||||
disablePreviewLinks={settings.disableResponsePreviewLinks}
|
||||
download={handleDownloadResponseBody}
|
||||
editorFontSize={settings.editorFontSize}
|
||||
error={response.error}
|
||||
error={activeResponse.error}
|
||||
filter={filter}
|
||||
filterHistory={filterHistory}
|
||||
getBody={handleGetResponseBody}
|
||||
previewMode={response.error ? PREVIEW_MODE_SOURCE : previewMode}
|
||||
responseId={response._id}
|
||||
updateFilter={response.error ? undefined : handleSetFilter}
|
||||
url={response.url}
|
||||
previewMode={activeResponse.error ? PREVIEW_MODE_SOURCE : previewMode}
|
||||
responseId={activeResponse._id}
|
||||
updateFilter={activeResponse.error ? undefined : handleSetFilter}
|
||||
url={activeResponse.url}
|
||||
/>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
@@ -189,15 +182,15 @@ export const ResponsePane: FC<Props> = ({
|
||||
title={
|
||||
<>
|
||||
Headers
|
||||
{response.headers.length > 0 && (
|
||||
<span className="bubble">{response.headers.length}</span>
|
||||
{activeResponse.headers.length > 0 && (
|
||||
<span className="bubble">{activeResponse.headers.length}</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PanelContainer className="pad">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ResponseHeadersViewer headers={response.headers} />
|
||||
<ErrorBoundary key={activeResponse._id} errorClassName="font-error pad text-center">
|
||||
<ResponseHeadersViewer headers={activeResponse.headers} />
|
||||
</ErrorBoundary>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
@@ -213,19 +206,19 @@ export const ResponsePane: FC<Props> = ({
|
||||
}
|
||||
>
|
||||
<PanelContainer className="pad">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ErrorBoundary key={activeResponse._id} errorClassName="font-error pad text-center">
|
||||
<ResponseCookiesViewer
|
||||
cookiesSent={response.settingSendCookies}
|
||||
cookiesStored={response.settingStoreCookies}
|
||||
cookiesSent={activeResponse.settingSendCookies}
|
||||
cookiesStored={activeResponse.settingStoreCookies}
|
||||
headers={cookieHeaders}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
<TabItem key="timeline" title="Timeline">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ErrorBoundary key={activeResponse._id} errorClassName="font-error pad text-center">
|
||||
<ResponseTimelineViewer
|
||||
key={response._id}
|
||||
key={activeResponse._id}
|
||||
timeline={timeline}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -4,7 +4,7 @@ import fs from 'fs';
|
||||
import { extension as mimeExtension } from 'mime-types';
|
||||
import path from 'path';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { useInterval } from 'react-use';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -12,8 +12,7 @@ import { database } from '../../common/database';
|
||||
import { getContentDispositionHeader } from '../../common/misc';
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../common/render';
|
||||
import * as models from '../../models';
|
||||
import { isEventStreamRequest, isRequest, Request } from '../../models/request';
|
||||
import { RequestMeta } from '../../models/request-meta';
|
||||
import { isEventStreamRequest, isRequest } from '../../models/request';
|
||||
import * as network from '../../network/network';
|
||||
import { convert } from '../../utils/importers/convert';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../utils/url/querystring';
|
||||
@@ -22,7 +21,7 @@ import { useReadyState } from '../hooks/use-ready-state';
|
||||
import { useRequestPatcher } from '../hooks/use-request';
|
||||
import { useRequestMetaPatcher } from '../hooks/use-request';
|
||||
import { useTimeoutWhen } from '../hooks/useTimeoutWhen';
|
||||
import { RequestLoaderData } from '../routes/request';
|
||||
import { ConnectActionParams, RequestLoaderData } from '../routes/request';
|
||||
import { RootLoaderData } from '../routes/root';
|
||||
import { WorkspaceLoaderData } from '../routes/workspace';
|
||||
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from './base/dropdown';
|
||||
@@ -69,10 +68,9 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
settings,
|
||||
} = useRouteLoaderData('root') as RootLoaderData;
|
||||
const { hotKeyRegistry } = settings;
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const downloadPath = activeRequestMeta.downloadPath;
|
||||
const patchRequestMeta = useRequestMetaPatcher();
|
||||
const { requestId } = useParams() as { requestId: string };
|
||||
const methodDropdownRef = useRef<DropdownHandle>(null);
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
const inputRef = useRef<OneLineEditorHandle>(null);
|
||||
@@ -191,12 +189,9 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
// Unset active response because we just made a new one
|
||||
// TODO: remove this with the redux fallback to first element
|
||||
await patchRequestMeta(activeRequest._id, { activeResponseId: null });
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeEnvironment._id, activeRequest, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion, patchRequestMeta]);
|
||||
}, [activeEnvironment._id, activeRequest, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (!activeRequest) {
|
||||
@@ -241,8 +236,17 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
await patchRequestMeta(activeRequest._id, { activeResponseId: null });
|
||||
setLoading(false);
|
||||
}, [activeEnvironment._id, activeRequest, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion, patchRequestMeta]);
|
||||
|
||||
const send = useCallback(() => {
|
||||
const fetcher = useFetcher();
|
||||
const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const connect = (connectParams: ConnectActionParams) => {
|
||||
fetcher.submit(JSON.stringify(connectParams),
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/connect`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const send = () => {
|
||||
setCurrentTimeout(undefined);
|
||||
if (downloadPath) {
|
||||
sendThenSetFilePath(downloadPath);
|
||||
@@ -262,9 +266,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
parameters: activeRequest.parameters.filter(p => !p.disabled),
|
||||
workspaceCookieJar,
|
||||
}, renderContext);
|
||||
window.main.curl.open({
|
||||
requestId: activeRequest._id,
|
||||
workspaceId,
|
||||
connect({
|
||||
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
@@ -275,7 +277,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
return;
|
||||
}
|
||||
handleSend();
|
||||
}, [activeEnvironment._id, activeWorkspace, downloadPath, handleSend, activeRequest, sendThenSetFilePath]);
|
||||
};
|
||||
|
||||
useInterval(send, currentInterval ? currentInterval : null);
|
||||
useTimeoutWhen(send, currentTimeout, !!currentTimeout);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { ChangeEventHandler, FC, ReactNode, useCallback } from 'react';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { SettingsOfType } from '../../../common/settings';
|
||||
import * as models from '../../../models/index';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
const Descriptions = styled.div({
|
||||
@@ -36,10 +36,7 @@ export const BooleanSetting: FC<{
|
||||
if (!settings.hasOwnProperty(setting)) {
|
||||
throw new Error(`Invalid boolean setting name ${setting}`);
|
||||
}
|
||||
|
||||
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(async ({ currentTarget: { checked } }) => {
|
||||
await models.settings.patch({ [setting]: checked });
|
||||
}, [setting]);
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -50,7 +47,7 @@ export const BooleanSetting: FC<{
|
||||
<input
|
||||
checked={Boolean(settings[setting])}
|
||||
name={setting}
|
||||
onChange={onChange}
|
||||
onChange={event => patchSettings({ [setting]: event.currentTarget.checked })}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { ChangeEventHandler, PropsWithChildren, ReactNode, useCallback } from 'react';
|
||||
import React, { PropsWithChildren, ReactNode } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { SettingsOfType } from '../../../common/settings';
|
||||
import * as models from '../../../models/index';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
interface Props<T> {
|
||||
@@ -25,9 +25,7 @@ export const EnumSetting = <T extends string | number>({
|
||||
settings,
|
||||
} = useRouteLoaderData('root') as RootLoaderData;
|
||||
|
||||
const onChange = useCallback<ChangeEventHandler<HTMLSelectElement>>(async ({ currentTarget: { value } }) => {
|
||||
await models.settings.patch({ [setting]: value });
|
||||
}, [setting]);
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
return (
|
||||
<div className="form-control form-control--outlined">
|
||||
@@ -37,7 +35,8 @@ export const EnumSetting = <T extends string | number>({
|
||||
<select
|
||||
value={String(settings[setting]) || '__NULL__'}
|
||||
name={setting}
|
||||
onChange={onChange}
|
||||
onChange={event => patchSettings({ [setting]: event.currentTarget.value })}
|
||||
|
||||
>
|
||||
{values.map(({ name, value }) => (
|
||||
<option key={value} value={value}>
|
||||
|
||||
@@ -8,15 +8,12 @@ import { docsImportExport } from '../../../common/documentation';
|
||||
import { exportAllToFile } from '../../../common/export';
|
||||
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
|
||||
import { strings } from '../../../common/strings';
|
||||
import { isRequestGroup } from '../../../models/request-group';
|
||||
import { selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
|
||||
import { selectWorkspacesForActiveProject } from '../../redux/selectors';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { Link } from '../base/link';
|
||||
import { AlertModal } from '../modals/alert-modal';
|
||||
import { ExportRequestsModal } from '../modals/export-requests-modal';
|
||||
import { ImportModal } from '../modals/import-modal';
|
||||
import { showModal } from '../modals/index';
|
||||
import { Button } from '../themed-button';
|
||||
interface Props {
|
||||
hideSettingsModal: () => void;
|
||||
@@ -34,20 +31,8 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
const projectName = workspaceData?.activeProject.name ?? getProductName();
|
||||
|
||||
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
|
||||
const workspaceRequestsAndRequestGroups = useSelector(selectWorkspaceRequestsAndRequestGroups);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
|
||||
const showExportRequestsModal = useCallback(() => {
|
||||
if (!workspaceRequestsAndRequestGroups.filter(r => !isRequestGroup(r)).length) {
|
||||
showModal(AlertModal, {
|
||||
title: 'Cannot export',
|
||||
message: <>There are no requests to export.</>,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showModal(ExportRequestsModal);
|
||||
hideSettingsModal();
|
||||
}, [hideSettingsModal, workspaceRequestsAndRequestGroups]);
|
||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||
|
||||
const handleExportAllToFile = useCallback(() => {
|
||||
exportAllToFile(projectName, workspacesForActiveProject);
|
||||
@@ -81,7 +66,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
<ItemContent
|
||||
icon="home"
|
||||
label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(workspaceData.activeWorkspace).singular}`}
|
||||
onClick={showExportRequestsModal}
|
||||
onClick={() => setIsExportModalOpen(true)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={`Export files from the "${projectName}" ${strings.project.singular}`}>
|
||||
@@ -94,7 +79,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
</DropdownSection>
|
||||
</Dropdown>) : (<Button onClick={handleExportAllToFile}>{`Export files from the "${projectName}" ${strings.project.singular}`}</Button>)
|
||||
}
|
||||
|
||||
|
||||
<Button
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -106,7 +91,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
<i className="fa fa-file-import" />
|
||||
{`Import to the "${projectName}" ${strings.project.singular}`}
|
||||
</Button>
|
||||
|
||||
|
||||
<Link href="https://insomnia.rest/create-run-button" className="btn btn--compact" button>
|
||||
Create Run Button
|
||||
</Link>
|
||||
@@ -124,6 +109,11 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
|
||||
defaultWorkspaceId={workspaceId}
|
||||
/>
|
||||
)}
|
||||
{isExportModalOpen && (
|
||||
<ExportRequestsModal
|
||||
onHide={() => setIsExportModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { ChangeEvent, FC } from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { SettingsOfType } from '../../../common/settings';
|
||||
import * as models from '../../../models';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
|
||||
@@ -29,12 +29,7 @@ export const MaskedSetting: FC<{
|
||||
if (!settings.hasOwnProperty(setting)) {
|
||||
throw new Error(`Invalid setting name ${setting}`);
|
||||
}
|
||||
|
||||
const onChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
await models.settings.patch({
|
||||
[setting]: event.currentTarget.value,
|
||||
});
|
||||
};
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -47,7 +42,7 @@ export const MaskedSetting: FC<{
|
||||
defaultValue={String(settings[setting])}
|
||||
disabled={disabled}
|
||||
name={setting}
|
||||
onChange={onChange}
|
||||
onChange={event => patchSettings({ [setting]: event.currentTarget.value })}
|
||||
placeholder={placeholder}
|
||||
type={!settings.showPasswords && isHidden ? 'password' : 'text'}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { snapNumberToLimits } from '../../../common/misc';
|
||||
import { SettingsOfType } from '../../../common/settings';
|
||||
import * as models from '../../../models/index';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
|
||||
@@ -31,6 +31,7 @@ export const NumberSetting: FC<Props> = ({
|
||||
if (!Object.prototype.hasOwnProperty.call(settings, setting)) {
|
||||
throw new Error(`Invalid setting name ${setting}`);
|
||||
}
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
const handleOnChange = useCallback<ChangeEventHandler<HTMLInputElement>>(async ({ currentTarget: { value, min, max } }) => {
|
||||
const updatedValue = snapNumberToLimits(
|
||||
@@ -38,8 +39,8 @@ export const NumberSetting: FC<Props> = ({
|
||||
parseInt(min, 10),
|
||||
parseInt(max, 10),
|
||||
);
|
||||
await models.settings.patch({ [setting]: updatedValue });
|
||||
}, [setting]);
|
||||
patchSettings({ [setting]: updatedValue });
|
||||
}, [patchSettings, setting]);
|
||||
|
||||
let defaultValue: string | number = settings[setting];
|
||||
if (typeof defaultValue !== 'number') {
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
PLUGIN_HUB_BASE,
|
||||
} from '../../../common/constants';
|
||||
import { docsPlugins } from '../../../common/documentation';
|
||||
import * as models from '../../../models';
|
||||
import { createPlugin } from '../../../plugins/create';
|
||||
import type { Plugin } from '../../../plugins/index';
|
||||
import { getPlugins } from '../../../plugins/index';
|
||||
import { reload } from '../../../templating/index';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { CopyButton } from '../base/copy-button';
|
||||
import { Link } from '../base/link';
|
||||
@@ -59,6 +59,7 @@ export const Plugins: FC = () => {
|
||||
|
||||
setState(state => ({ ...state, plugins, isRefreshingPlugins: false }));
|
||||
}
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -91,9 +92,7 @@ export const Plugins: FC = () => {
|
||||
onChange={async event => {
|
||||
const newConfig = { ...plugin.config, disabled: !event.target.checked };
|
||||
setState(state => ({ ...state, isRefreshingPlugins: true }));
|
||||
await models.settings.update(settings, {
|
||||
pluginConfig: { ...settings.pluginConfig, [plugin.name]: newConfig },
|
||||
});
|
||||
patchSettings({ pluginConfig: { ...settings.pluginConfig, [plugin.name]: newConfig } });
|
||||
refreshPlugins();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
newDefaultRegistry,
|
||||
} from '../../../common/hotkeys';
|
||||
import { HotKeyRegistry, KeyboardShortcut, KeyCombination } from '../../../common/settings';
|
||||
import * as models from '../../../models/index';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
@@ -27,12 +27,13 @@ export const Shortcuts: FC = () => {
|
||||
settings,
|
||||
} = useRouteLoaderData('root') as RootLoaderData;
|
||||
const { hotKeyRegistry } = settings;
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
return (
|
||||
<div className="shortcuts">
|
||||
<div className="row-spaced margin-bottom-xs">
|
||||
<div>
|
||||
<PromptButton className="btn btn--clicky" onClick={() => models.settings.update(settings, { hotKeyRegistry: newDefaultRegistry() })}>
|
||||
<PromptButton className="btn btn--clicky" onClick={() => patchSettings({ hotKeyRegistry: newDefaultRegistry() })}>
|
||||
Reset all
|
||||
</PromptButton>
|
||||
</div>
|
||||
@@ -84,7 +85,7 @@ export const Shortcuts: FC = () => {
|
||||
addKeyCombination: (keyboardShortcut: KeyboardShortcut, keyComb: KeyCombination) => {
|
||||
const keyCombs = getPlatformKeyCombinations(hotKeyRegistry[keyboardShortcut]);
|
||||
keyCombs.push(keyComb);
|
||||
models.settings.update(settings, { hotKeyRegistry });
|
||||
patchSettings({ hotKeyRegistry });
|
||||
},
|
||||
}
|
||||
)}
|
||||
@@ -116,7 +117,7 @@ export const Shortcuts: FC = () => {
|
||||
});
|
||||
if (toBeRemovedIndex >= 0) {
|
||||
keyCombosForThisPlatform.splice(toBeRemovedIndex, 1);
|
||||
models.settings.update(settings, { hotKeyRegistry });
|
||||
patchSettings({ hotKeyRegistry });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -134,7 +135,7 @@ export const Shortcuts: FC = () => {
|
||||
withPrompt
|
||||
onClick={() => {
|
||||
hotKeyRegistry[keyboardShortcut] = newDefaultRegistry()[keyboardShortcut];
|
||||
models.settings.update(settings, { hotKeyRegistry });
|
||||
patchSettings({ hotKeyRegistry });
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { ChangeEventHandler, FC, InputHTMLAttributes, useCallback } from
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { SettingsOfType } from '../../../common/settings';
|
||||
import * as models from '../../../models/index';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
|
||||
@@ -26,11 +26,12 @@ export const TextSetting: FC<{
|
||||
if (!Object.prototype.hasOwnProperty.call(settings, setting)) {
|
||||
throw new Error(`Invalid setting name ${setting}`);
|
||||
}
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
const handleOnChange = useCallback<ChangeEventHandler<HTMLInputElement>>(async ({ currentTarget: { value } }) => {
|
||||
const updatedValue = value === null ? '__NULL__' : value;
|
||||
await models.settings.patch({ [setting]: updatedValue });
|
||||
}, [setting]);
|
||||
patchSettings({ [setting]: updatedValue });
|
||||
}, [patchSettings, setting]);
|
||||
|
||||
let defaultValue = settings[setting];
|
||||
if (typeof defaultValue !== 'string') {
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { GrpcRequest } from '../../../models/grpc-request';
|
||||
import { Request } from '../../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { selectSidebarChildren } from '../../redux/selectors';
|
||||
import { isRequestGroup } from '../../../models/request-group';
|
||||
import { Child, WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { SidebarCreateDropdown } from './sidebar-create-dropdown';
|
||||
import { SidebarRequestGroupRow } from './sidebar-request-group-row';
|
||||
import { SidebarRequestRow } from './sidebar-request-row';
|
||||
|
||||
export interface Child {
|
||||
doc: Request | GrpcRequest | WebSocketRequest | RequestGroup;
|
||||
children: Child[];
|
||||
collapsed: boolean;
|
||||
hidden: boolean;
|
||||
pinned: boolean;
|
||||
}
|
||||
export interface SidebarChildObjects {
|
||||
pinned: Child[];
|
||||
all: Child[];
|
||||
@@ -34,10 +23,12 @@ interface Props {
|
||||
export const SidebarChildren: FC<Props> = ({
|
||||
filter,
|
||||
}) => {
|
||||
const sidebarChildren = useSelector(selectSidebarChildren);
|
||||
const {
|
||||
requestTree,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
|
||||
const { all, pinned } = sidebarChildren;
|
||||
const showSeparator = sidebarChildren.pinned.length > 0;
|
||||
const pinned = requestTree.filter((child: Child) => child.pinned);
|
||||
const showSeparator = pinned.length > 0;
|
||||
const contextMenuPortal = ReactDOM.createPortal(
|
||||
<div className="hide">
|
||||
<SidebarCreateDropdown />
|
||||
@@ -62,7 +53,7 @@ export const SidebarChildren: FC<Props> = ({
|
||||
<RecursiveSidebarRows
|
||||
filter={filter}
|
||||
isInPinnedList={false}
|
||||
rows={all}
|
||||
rows={requestTree}
|
||||
/>
|
||||
</ul>
|
||||
{contextMenuPortal}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { DragSource, DragSourceSpec, DropTarget, DropTargetMonitor, DropTargetSp
|
||||
|
||||
import * as models from '../../../models/index';
|
||||
import { RequestGroup } from '../../../models/request-group';
|
||||
import { useRequestGroupMetaPatcher } from '../../hooks/use-request';
|
||||
import { Highlight } from '../base/highlight';
|
||||
import { RequestGroupActionsDropdown, RequestGroupActionsDropdownHandle } from '../dropdowns/request-group-actions-dropdown';
|
||||
import { showModal } from '../modals';
|
||||
import { RequestGroupSettingsModal } from '../modals/request-group-settings-modal';
|
||||
import { DnDDragProps, DnDDropProps, DnDProps, DragObject, dropHandleCreator, hoverHandleCreator, sourceCollect, targetCollect } from './dnd';
|
||||
import { SidebarRequestRow } from './sidebar-request-row';
|
||||
@@ -37,7 +37,7 @@ export const SidebarRequestGroupRowFC = forwardRef<SidebarRequestGroupRowHandle,
|
||||
const [dragDirection, setDragDirection] = useState(0);
|
||||
const dropdownRef = useRef<RequestGroupActionsDropdownHandle>(null);
|
||||
const expandTagRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isRequestGroupSettingsModalOpen, setIsRequestGroupSettingsModalOpen] = useState(false);
|
||||
useImperativeHandle(ref, () => ({
|
||||
setDragDirection,
|
||||
getExpandTag:() => expandTagRef.current,
|
||||
@@ -51,17 +51,13 @@ export const SidebarRequestGroupRowFC = forwardRef<SidebarRequestGroupRowHandle,
|
||||
'sidebar__row--dragging-above': isDraggingOver && dragDirection > 0,
|
||||
'sidebar__row--dragging-below': isDraggingOver && dragDirection < 0,
|
||||
});
|
||||
const groupMetaPatcher = useRequestGroupMetaPatcher();
|
||||
// NOTE: We only want the button draggable, not the whole container (ie. no children)
|
||||
const button = connectDragSource(
|
||||
connectDropTarget(
|
||||
<button
|
||||
onClick={async () => {
|
||||
const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroup._id);
|
||||
if (requestGroupMeta) {
|
||||
models.requestGroupMeta.update(requestGroupMeta, { collapsed: !isCollapsed });
|
||||
return;
|
||||
}
|
||||
models.requestGroupMeta.create({ parentId: requestGroup._id, collapsed: false });
|
||||
groupMetaPatcher(requestGroup._id, { collapsed: !isCollapsed });
|
||||
}}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
@@ -99,9 +95,13 @@ export const SidebarRequestGroupRowFC = forwardRef<SidebarRequestGroupRowHandle,
|
||||
<div className="sidebar__actions">
|
||||
<RequestGroupActionsDropdown
|
||||
ref={dropdownRef}
|
||||
handleShowSettings={() => showModal(RequestGroupSettingsModal, { requestGroup })}
|
||||
handleShowSettings={() => setIsRequestGroupSettingsModalOpen(true)}
|
||||
requestGroup={requestGroup}
|
||||
/>
|
||||
{isRequestGroupSettingsModalOpen && <RequestGroupSettingsModal
|
||||
requestGroup={requestGroup}
|
||||
onHide={() => setIsRequestGroupSettingsModalOpen(false)}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
<ul className={classnames('sidebar__list', { 'sidebar__list--collapsed': isCollapsed })}>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Editable } from '../base/editable';
|
||||
import { Highlight } from '../base/highlight';
|
||||
import { RequestActionsDropdown } from '../dropdowns/request-actions-dropdown';
|
||||
import { WebSocketRequestActionsDropdown } from '../dropdowns/websocket-request-actions-dropdown';
|
||||
import { showModal, showPrompt } from '../modals/index';
|
||||
import { showPrompt } from '../modals/index';
|
||||
import { RequestSettingsModal } from '../modals/request-settings-modal';
|
||||
import { GrpcTag } from '../tags/grpc-tag';
|
||||
import { MethodTag } from '../tags/method-tag';
|
||||
@@ -144,9 +144,7 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
|
||||
});
|
||||
}, [requestGroup?._id, requestFetcher, organizationId, projectId, workspaceId]);
|
||||
|
||||
const handleShowRequestSettings = useCallback(() => {
|
||||
request && showModal(RequestSettingsModal, { request });
|
||||
}, [request]);
|
||||
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
|
||||
|
||||
const [methodOverrideValue, setMethodOverrideValue] = useState<string | null>(null);
|
||||
|
||||
@@ -270,27 +268,32 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
|
||||
</div>
|
||||
</button>
|
||||
<div className="sidebar__actions">
|
||||
{isWebSocketRequest(request) ? (
|
||||
<WebSocketRequestActionsDropdown
|
||||
{isWebSocketRequest(request) ?
|
||||
(<WebSocketRequestActionsDropdown
|
||||
ref={requestActionsDropdown}
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
request={request}
|
||||
isPinned={isPinned}
|
||||
handleShowSettings={handleShowRequestSettings}
|
||||
/>
|
||||
) : (
|
||||
<RequestActionsDropdown
|
||||
handleShowSettings={() => setIsRequestSettingsModalOpen(true)}
|
||||
/>)
|
||||
:
|
||||
(<RequestActionsDropdown
|
||||
ref={requestActionsDropdown}
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
handleShowSettings={handleShowRequestSettings}
|
||||
handleShowSettings={() => setIsRequestSettingsModalOpen(true)}
|
||||
request={request}
|
||||
isPinned={isPinned}
|
||||
requestGroup={requestGroup}
|
||||
activeEnvironment={activeEnvironment}
|
||||
activeProject={activeProject}
|
||||
/>
|
||||
)}
|
||||
/>)}
|
||||
</div>
|
||||
{isRequestSettingsModalOpen && (
|
||||
<RequestSettingsModal
|
||||
request={request}
|
||||
onHide={() => setIsRequestSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isPinned && (
|
||||
<div className="sidebar__item__icon-pin">
|
||||
<i className="fa fa-thumb-tack" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Cookie } from 'tough-cookie';
|
||||
|
||||
import { showCookiesModal } from '../modals/cookies-modal';
|
||||
import { CookiesModal } from '../modals/cookies-modal';
|
||||
|
||||
interface Props {
|
||||
cookiesSent?: boolean | null;
|
||||
@@ -10,6 +10,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ResponseCookiesViewer: FC<Props> = props => {
|
||||
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
|
||||
const renderRow = (h: any, i: number) => {
|
||||
let cookie: Cookie | undefined | null = null;
|
||||
|
||||
@@ -59,9 +60,14 @@ export const ResponseCookiesViewer: FC<Props> = props => {
|
||||
<tbody>{!headers.length ? renderRow(null, -1) : headers.map(renderRow)}</tbody>
|
||||
</table>
|
||||
<p className="pad-top">
|
||||
<button className="pull-right btn btn--clicky" onClick={showCookiesModal}>
|
||||
<button className="pull-right btn btn--clicky" onClick={() => setIsCookieModalOpen(true)}>
|
||||
Manage Cookies
|
||||
</button>
|
||||
</p>
|
||||
{isCookieModalOpen && (
|
||||
<CookiesModal
|
||||
onHide={() => setIsCookieModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { FC, useCallback, useLayoutEffect, useRef } from 'react';
|
||||
import React, { FC, useLayoutEffect, useRef } from 'react';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import * as models from '../../../models';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
|
||||
import { ConnectActionParams } from '../../routes/request';
|
||||
import { OneLineEditor, OneLineEditorHandle } from '../codemirror/one-line-editor';
|
||||
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder';
|
||||
import { showAlert, showModal } from '../modals';
|
||||
@@ -25,7 +27,6 @@ const Button = styled.button<{ warning?: boolean }>(({ warning }) => ({
|
||||
|
||||
interface ActionBarProps {
|
||||
request: WebSocketRequest;
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
defaultValue: string;
|
||||
readyState: boolean;
|
||||
@@ -66,13 +67,24 @@ export const ConnectionCircle = styled.span({
|
||||
borderRadius: '50%',
|
||||
});
|
||||
|
||||
export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, environmentId, defaultValue, onChange, readyState }) => {
|
||||
export const WebSocketActionBar: FC<ActionBarProps> = ({ request, environmentId, defaultValue, onChange, readyState }) => {
|
||||
const isOpen = readyState;
|
||||
const oneLineEditorRef = useRef<OneLineEditorHandle>(null);
|
||||
useLayoutEffect(() => {
|
||||
oneLineEditorRef.current?.focusEnd();
|
||||
}, []);
|
||||
const handleSubmit = useCallback(async () => {
|
||||
|
||||
const fetcher = useFetcher();
|
||||
const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const connect = (connectParams: ConnectActionParams) => {
|
||||
fetcher.submit(JSON.stringify(connectParams),
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/connect`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
if (isOpen) {
|
||||
window.main.webSocket.close({ requestId: request._id });
|
||||
return;
|
||||
@@ -88,9 +100,7 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, e
|
||||
parameters: request.parameters.filter(p => !p.disabled),
|
||||
workspaceCookieJar,
|
||||
}, renderContext);
|
||||
window.main.webSocket.open({
|
||||
requestId: request._id,
|
||||
workspaceId,
|
||||
connect({
|
||||
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
@@ -116,7 +126,7 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, e
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [environmentId, isOpen, request, workspaceId]);
|
||||
};
|
||||
|
||||
useDocBodyKeyboardShortcuts({
|
||||
request_send: () => handleSubmit(),
|
||||
|
||||
@@ -7,7 +7,6 @@ import { PREVIEW_MODE_FRIENDLY, PREVIEW_MODE_RAW, PREVIEW_MODE_SOURCE, PreviewMo
|
||||
import { CurlEvent, CurlMessageEvent } from '../../../main/network/curl';
|
||||
import { WebSocketEvent, WebSocketMessageEvent } from '../../../main/network/websocket';
|
||||
import { requestMeta } from '../../../models';
|
||||
import { RequestMeta } from '../../../models/request-meta';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { showError } from '../modals';
|
||||
@@ -94,7 +93,7 @@ export const MessageEventView: FC<Props<CurlMessageEvent | WebSocketMessageEvent
|
||||
} catch {
|
||||
// Can't parse as JSON.
|
||||
}
|
||||
const { activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<any, RequestMeta>;
|
||||
const { activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const previewMode = activeRequestMeta.previewMode || PREVIEW_MODE_SOURCE;
|
||||
return (
|
||||
<PreviewPane>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getSetCookieHeaders } from '../../../common/misc';
|
||||
@@ -10,7 +10,7 @@ import { WebSocketEvent } from '../../../main/network/websocket';
|
||||
import { Response } from '../../../models/response';
|
||||
import { WebSocketResponse } from '../../../models/websocket-response';
|
||||
import { useRealtimeConnectionEvents } from '../../hooks/use-realtime-connection-events';
|
||||
import { selectActiveResponse } from '../../redux/selectors';
|
||||
import { RequestLoaderData, WebSocketRequestLoaderData } from '../../routes/request';
|
||||
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
|
||||
import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
@@ -85,8 +85,9 @@ const PaddedButton = styled('button')({
|
||||
});
|
||||
|
||||
export const RealtimeResponsePane: FC<{ requestId: string }> = () => {
|
||||
const response = useSelector(selectActiveResponse) as WebSocketResponse | Response | null;
|
||||
if (!response) {
|
||||
const { activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData;
|
||||
|
||||
if (!activeResponse) {
|
||||
return (
|
||||
<Pane type="response">
|
||||
<PaneHeader />
|
||||
@@ -94,7 +95,7 @@ export const RealtimeResponsePane: FC<{ requestId: string }> = () => {
|
||||
</Pane>
|
||||
);
|
||||
}
|
||||
return <RealtimeActiveResponsePane response={response} />;
|
||||
return <RealtimeActiveResponsePane response={activeResponse} />;
|
||||
};
|
||||
|
||||
const RealtimeActiveResponsePane: FC<{ response: WebSocketResponse | Response }> = ({
|
||||
@@ -175,7 +176,6 @@ const RealtimeActiveResponsePane: FC<{ response: WebSocketResponse | Response }>
|
||||
</div>
|
||||
<ResponseHistoryDropdown
|
||||
activeResponse={response}
|
||||
className="tall pane__header__right"
|
||||
/>
|
||||
</PaneHeader>
|
||||
<Tabs aria-label="Curl response pane tabs">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -6,13 +6,12 @@ import { AuthType, CONTENT_TYPE_JSON } from '../../../common/constants';
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import * as models from '../../../models';
|
||||
import { Environment } from '../../../models/environment';
|
||||
import { RequestMeta } from '../../../models/request-meta';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
|
||||
import { useReadyState } from '../../hooks/use-ready-state';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { WebSocketRequestLoaderData } from '../../routes/request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
import { TabItem, Tabs } from '../base/tabs';
|
||||
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
|
||||
@@ -202,7 +201,7 @@ interface Props {
|
||||
// currently this is blocked by the way page layout divide the panes with dragging functionality
|
||||
// TODO: @gatzjames discuss above assertion in light of request and settings drills
|
||||
export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<WebSocketRequest, RequestMeta>;
|
||||
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as WebSocketRequestLoaderData;
|
||||
|
||||
const { workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const readyState = useReadyState({ requestId: activeRequest._id, protocol: 'webSocket' });
|
||||
@@ -247,14 +246,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditDescription = useCallback(() => {
|
||||
showModal(RequestSettingsModal, { request: activeRequest });
|
||||
}, [activeRequest]);
|
||||
|
||||
const handleEditDescriptionAdd = useCallback(() => {
|
||||
showModal(RequestSettingsModal, { request: activeRequest, forceEditMode: true });
|
||||
}, [activeRequest]);
|
||||
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
|
||||
|
||||
const gitVersion = useGitVCSVersion();
|
||||
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
|
||||
@@ -268,7 +260,6 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
<WebSocketActionBar
|
||||
key={uniqueKey}
|
||||
request={activeRequest}
|
||||
workspaceId={workspaceId}
|
||||
environmentId={environment?._id || ''}
|
||||
defaultValue={activeRequest.url}
|
||||
readyState={readyState}
|
||||
@@ -360,7 +351,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
{activeRequest.description ? (
|
||||
<div>
|
||||
<div className="pull-right pad bg-default">
|
||||
<button className="btn btn--clicky" onClick={handleEditDescription}>
|
||||
<button className="btn btn--clicky" onClick={() => setIsRequestSettingsModalOpen(true)}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
@@ -387,10 +378,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<button
|
||||
className="btn btn--clicky faint"
|
||||
onClick={handleEditDescriptionAdd}
|
||||
>
|
||||
<button className="btn btn--clicky faint" onClick={() => setIsRequestSettingsModalOpen(true)}>
|
||||
Add Description
|
||||
</button>
|
||||
</p>
|
||||
@@ -398,6 +386,12 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
)}
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
{isRequestSettingsModalOpen && (
|
||||
<RequestSettingsModal
|
||||
request={activeRequest}
|
||||
onHide={() => setIsRequestSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useCallback } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { getRenderContext, getRenderContextAncestors, HandleGetRenderContext, HandleRender, render } from '../../../common/render';
|
||||
import { Request } from '../../../models/request';
|
||||
import { NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME } from '../../../templating';
|
||||
import { getKeys } from '../../../templating/utils';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
@@ -19,7 +18,7 @@ initializeNunjucksRenderPromiseCache();
|
||||
* Access to functions useful for Nunjucks rendering
|
||||
*/
|
||||
export const useNunjucks = () => {
|
||||
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any> | undefined;
|
||||
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData | undefined;
|
||||
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
|
||||
const fetchRenderContext = useCallback(async () => {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { ChangeEvent, useCallback, useState } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import * as models from '../../models';
|
||||
import { ThemeSettings } from '../../models/settings';
|
||||
import { ColorScheme, getThemes } from '../../plugins';
|
||||
import { applyColorScheme, PluginTheme } from '../../plugins/misc';
|
||||
import { RootLoaderData } from '../routes/root';
|
||||
import { useSettingsPatcher } from './use-request';
|
||||
|
||||
export const useThemes = () => {
|
||||
const {
|
||||
@@ -38,6 +38,7 @@ export const useThemes = () => {
|
||||
}
|
||||
return pluginTheme.name === theme;
|
||||
}, [autoDetectColorScheme, isActiveDark, isActiveLight, theme]);
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
// Apply the theme and update settings
|
||||
const apply = useCallback(async (patch: Partial<ThemeSettings>) => {
|
||||
@@ -48,8 +49,9 @@ export const useThemes = () => {
|
||||
lightTheme,
|
||||
...patch,
|
||||
});
|
||||
await models.settings.patch(patch);
|
||||
}, [autoDetectColorScheme, darkTheme, lightTheme, theme]);
|
||||
patchSettings(patch);
|
||||
|
||||
}, [autoDetectColorScheme, darkTheme, lightTheme, patchSettings, theme]);
|
||||
|
||||
const changeAutoDetect = useCallback(({ target: { checked } }: ChangeEvent<HTMLInputElement>) => apply({ autoDetectColorScheme: checked }), [apply]);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { getProductName } from '../../common/constants';
|
||||
import { Request } from '../../models/request';
|
||||
import { RequestLoaderData } from '../routes/request';
|
||||
import { WorkspaceLoaderData } from '../routes/workspace';
|
||||
export const useDocumentTitle = () => {
|
||||
@@ -12,7 +11,7 @@ export const useDocumentTitle = () => {
|
||||
activeProject,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
|
||||
// Update document title
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,17 +7,20 @@ import { showModal } from '../components/modals';
|
||||
import { SettingsModal, TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal';
|
||||
import { RootLoaderData } from '../routes/root';
|
||||
import { WorkspaceLoaderData } from '../routes/workspace';
|
||||
import { useSettingsPatcher } from './use-request';
|
||||
export const useGlobalKeyboardShortcuts = () => {
|
||||
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
|
||||
const {
|
||||
settings,
|
||||
} = useRouteLoaderData('root') as RootLoaderData;
|
||||
const { activeWorkspaceMeta } = workspaceData || {};
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
useDocBodyKeyboardShortcuts({
|
||||
plugin_reload:
|
||||
() => plugins.reloadPlugins(),
|
||||
environment_showVariableSourceAndValue:
|
||||
() => models.settings.update(settings, { showVariableSourceAndValue: !settings.showVariableSourceAndValue }),
|
||||
() => patchSettings({ showVariableSourceAndValue: !settings.showVariableSourceAndValue }),
|
||||
preferences_showGeneral:
|
||||
() => showModal(SettingsModal),
|
||||
preferences_showKeyboardShortcuts:
|
||||
|
||||
@@ -4,14 +4,17 @@ import { useFetcher, useParams } from 'react-router-dom';
|
||||
import { GrpcRequest } from '../../models/grpc-request';
|
||||
import { GrpcRequestMeta } from '../../models/grpc-request-meta';
|
||||
import { Request } from '../../models/request';
|
||||
import { RequestGroup } from '../../models/request-group';
|
||||
import { RequestGroupMeta } from '../../models/request-group-meta';
|
||||
import { RequestMeta } from '../../models/request-meta';
|
||||
import { Settings } from '../../models/settings';
|
||||
import { WebSocketRequest } from '../../models/websocket-request';
|
||||
|
||||
export const useRequestPatcher = () => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const requestMetaFetcher = useFetcher();
|
||||
const fetcher = useFetcher();
|
||||
return (requestId: string, patch: Partial<GrpcRequest> | Partial<Request> | Partial<WebSocketRequest>) => {
|
||||
requestMetaFetcher.submit(JSON.stringify(patch), {
|
||||
fetcher.submit(JSON.stringify(patch), {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/update`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
@@ -21,9 +24,9 @@ export const useRequestPatcher = () => {
|
||||
|
||||
export const useRequestMetaPatcher = () => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const requestMetaFetcher = useFetcher();
|
||||
const fetcher = useFetcher();
|
||||
return (requestId: string, patch: Partial<GrpcRequestMeta> | Partial<RequestMeta>) => {
|
||||
requestMetaFetcher.submit(patch, {
|
||||
fetcher.submit(patch, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/update-meta`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
@@ -31,4 +34,39 @@ export const useRequestMetaPatcher = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useRequestGroupPatcher = () => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const fetcher = useFetcher();
|
||||
return (requestGroupId: string, patch: Partial<RequestGroup>) => {
|
||||
fetcher.submit(JSON.stringify(patch), {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${requestGroupId}/update`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const useRequestGroupMetaPatcher = () => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const fetcher = useFetcher();
|
||||
return (requestGroupId: string, patch: Partial<RequestGroupMeta>) => {
|
||||
fetcher.submit(patch, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${requestGroupId}/update-meta`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const useSettingsPatcher = () => {
|
||||
const fetcher = useFetcher();
|
||||
return (patch: Partial<Settings>) => {
|
||||
fetcher.submit(JSON.stringify(patch), {
|
||||
action: '/settings/update',
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export type CreateRequestType = 'HTTP' | 'gRPC' | 'GraphQL' | 'WebSocket' | 'Event Stream';
|
||||
|
||||
@@ -79,6 +79,10 @@ const router = createMemoryRouter(
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'settings/update',
|
||||
action: async (...args) => (await import('./routes/actions')).updateSettingsAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'organization',
|
||||
children: [
|
||||
@@ -139,6 +143,7 @@ const router = createMemoryRouter(
|
||||
children: [
|
||||
{
|
||||
path: `${ACTIVITY_DEBUG}`,
|
||||
loader: async (...args) => (await import('./routes/debug')).loader(...args),
|
||||
element: (
|
||||
<Suspense fallback={<AppLoadingIndicator />}>
|
||||
<Debug />
|
||||
@@ -150,6 +155,10 @@ const router = createMemoryRouter(
|
||||
id: 'request/:requestId',
|
||||
loader: async (...args) => (await import('./routes/request')).loader(...args),
|
||||
children: [
|
||||
{
|
||||
path: 'connect',
|
||||
action: async (...args) => (await import('./routes/request')).connectAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'duplicate',
|
||||
action: async (...args) => (await import('./routes/request')).duplicateRequestAction(...args),
|
||||
@@ -162,6 +171,14 @@ const router = createMemoryRouter(
|
||||
path: 'update-meta',
|
||||
action: async (...args) => (await import('./routes/request')).updateRequestMetaAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'response/delete-all',
|
||||
action: async (...args) => (await import('./routes/request')).deleteAllResponsesAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'response/delete',
|
||||
action: async (...args) => (await import('./routes/request')).deleteResponseAction(...args),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -184,6 +201,10 @@ const router = createMemoryRouter(
|
||||
path: 'request-group/update',
|
||||
action: async (...args) => (await import('./routes/request-group')).updateRequestGroupAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'request-group/:requestGroupId/update-meta',
|
||||
action: async (...args) => (await import('./routes/request-group')).updateRequestGroupMetaAction(...args),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import type { ValueOf } from 'type-fest';
|
||||
|
||||
import { fuzzyMatchAll } from '../../common/misc';
|
||||
import * as models from '../../models';
|
||||
import { BaseModel } from '../../models';
|
||||
import { GrpcRequest, isGrpcRequest } from '../../models/grpc-request';
|
||||
import { getStatusCandidates } from '../../models/helpers/get-status-candidates';
|
||||
import { sortProjects } from '../../models/helpers/project';
|
||||
import { DEFAULT_PROJECT_ID, isRemoteProject } from '../../models/project';
|
||||
import { isRequest, Request } from '../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../models/request-group';
|
||||
import { type Response } from '../../models/response';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../models/websocket-request';
|
||||
import { type WebSocketResponse } from '../../models/websocket-response';
|
||||
import { BaseModel, canSync } from '../../models';
|
||||
import { DEFAULT_PROJECT_ID } from '../../models/project';
|
||||
import { isRequest } from '../../models/request';
|
||||
import { StatusCandidate } from '../../sync/types';
|
||||
import { RootState } from './modules';
|
||||
|
||||
type EntitiesLists = {
|
||||
@@ -31,28 +23,14 @@ const selectEntitiesLists = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const selectRequestMetas = createSelector(
|
||||
selectEntitiesLists,
|
||||
entities => entities.requestMetas,
|
||||
);
|
||||
|
||||
export const selectGrpcRequestMetas = createSelector(
|
||||
selectEntitiesLists,
|
||||
entities => entities.grpcRequestMetas,
|
||||
);
|
||||
|
||||
export const selectRemoteProjects = createSelector(
|
||||
selectEntitiesLists,
|
||||
entities => sortProjects(entities.projects).filter(isRemoteProject),
|
||||
);
|
||||
|
||||
// list workspaces for move/copy switcher, and export
|
||||
export const selectWorkspacesForActiveProject = createSelector(
|
||||
selectEntitiesLists,
|
||||
(state: RootState) => state.global.activeProjectId,
|
||||
(entities, activeProjectId) => entities.workspaces.filter(workspace => workspace.parentId === (activeProjectId || DEFAULT_PROJECT_ID)),
|
||||
);
|
||||
|
||||
export const selectActiveWorkspace = createSelector(
|
||||
const selectActiveWorkspace = createSelector(
|
||||
selectEntitiesLists,
|
||||
(state: RootState) => state.global.activeWorkspaceId,
|
||||
(state: RootState) => state.global.activeProjectId,
|
||||
@@ -62,11 +40,6 @@ export const selectActiveWorkspace = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const selectRequestVersions = createSelector(
|
||||
selectEntitiesLists,
|
||||
entities => entities.requestVersions,
|
||||
);
|
||||
|
||||
const selectEntitiesChildrenMap = createSelector(selectEntitiesLists, entities => {
|
||||
const parentLookupMap: any = {};
|
||||
// group entities by parent
|
||||
@@ -114,235 +87,12 @@ const selectActiveWorkspaceEntities = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const selectWorkspaceRequestsAndRequestGroups = createSelector(
|
||||
selectActiveWorkspaceEntities,
|
||||
entities => {
|
||||
return entities.filter(
|
||||
entity => isRequest(entity) || isWebSocketRequest(entity) || isGrpcRequest(entity) || isRequestGroup(entity),
|
||||
) as (Request | WebSocketRequest | GrpcRequest | RequestGroup)[];
|
||||
},
|
||||
);
|
||||
|
||||
const selectActiveWorkspaceMeta = createSelector(
|
||||
selectActiveWorkspace,
|
||||
selectEntitiesLists,
|
||||
(activeWorkspace, entities) => {
|
||||
return entities.workspaceMetas.find(workspaceMeta => workspaceMeta.parentId === activeWorkspace?._id);
|
||||
},
|
||||
);
|
||||
export const selectActiveRequest = createSelector(
|
||||
(state: RootState) => state.entities,
|
||||
selectActiveWorkspaceMeta,
|
||||
(entities, workspaceMeta) => {
|
||||
const id = workspaceMeta?.activeRequestId || 'n/a';
|
||||
|
||||
if (id in entities.requests) {
|
||||
return entities.requests[id];
|
||||
}
|
||||
|
||||
if (id in entities.grpcRequests) {
|
||||
return entities.grpcRequests[id];
|
||||
}
|
||||
|
||||
if (id in entities.webSocketRequests) {
|
||||
return entities.webSocketRequests[id];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
export const selectActiveRequestResponses = createSelector(
|
||||
selectActiveRequest,
|
||||
selectEntitiesLists,
|
||||
selectActiveWorkspace,
|
||||
(activeRequest, entities, activeWorkspace) => {
|
||||
const requestId = activeRequest ? activeRequest._id : 'n/a';
|
||||
|
||||
const responses: (Response | WebSocketResponse)[] = (activeRequest && isWebSocketRequest(activeRequest)) ? entities.webSocketResponses : entities.responses;
|
||||
|
||||
// Filter responses down if the setting is enabled
|
||||
return responses.filter(response => {
|
||||
const requestMatches = requestId === response.parentId;
|
||||
|
||||
if ((entities.settings[0] || models.settings.init()).filterResponsesByEnv) {
|
||||
const meta = entities.workspaceMetas.find(workspaceMeta => workspaceMeta.parentId === activeWorkspace?._id);
|
||||
const activeEnvironment = meta ? entities.environments.find(environment => environment._id === meta.activeEnvironmentId) : null;
|
||||
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
|
||||
const environmentMatches = response.environmentId === activeEnvironmentId;
|
||||
return requestMatches && environmentMatches;
|
||||
} else {
|
||||
return requestMatches;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => (a.created > b.created ? -1 : 1));
|
||||
},
|
||||
);
|
||||
const selectActiveRequestMeta = createSelector(
|
||||
selectActiveRequest,
|
||||
selectEntitiesLists,
|
||||
(activeRequest, entities) => {
|
||||
const id = activeRequest?._id || 'n/a';
|
||||
return entities.requestMetas.find(m => m.parentId === id);
|
||||
},
|
||||
);
|
||||
export const selectActiveResponse = createSelector(
|
||||
selectActiveRequestMeta,
|
||||
selectActiveRequestResponses,
|
||||
(activeRequestMeta, responses) => {
|
||||
const activeResponseId = activeRequestMeta ? activeRequestMeta.activeResponseId : 'n/a';
|
||||
|
||||
const activeResponse = responses.find(response => response._id === activeResponseId);
|
||||
|
||||
if (activeResponse) {
|
||||
return activeResponse;
|
||||
}
|
||||
|
||||
return responses[0] || null;
|
||||
},
|
||||
);
|
||||
|
||||
// sync dropdown, branches, history and staging
|
||||
export const selectSyncItems = createSelector(
|
||||
selectActiveWorkspaceEntities,
|
||||
getStatusCandidates,
|
||||
);
|
||||
|
||||
type SidebarModel = Request | GrpcRequest | RequestGroup;
|
||||
|
||||
const shouldShowInSidebar = (model: BaseModel): boolean =>
|
||||
isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model) || isRequestGroup(model);
|
||||
|
||||
const shouldIgnoreChildrenOf = (model: SidebarModel): boolean =>
|
||||
isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model);
|
||||
|
||||
const sortByMetaKeyOrId = (a: SidebarModel, b: SidebarModel): number => {
|
||||
if (a.metaSortKey === b.metaSortKey) {
|
||||
return a._id > b._id ? -1 : 1; // ascending
|
||||
} else {
|
||||
return a.metaSortKey < b.metaSortKey ? -1 : 1; // descending
|
||||
}
|
||||
};
|
||||
|
||||
interface Child {
|
||||
doc: SidebarModel;
|
||||
hidden: boolean;
|
||||
collapsed: boolean;
|
||||
pinned: boolean;
|
||||
children: Child[];
|
||||
}
|
||||
|
||||
export interface SidebarChildren {
|
||||
all: Child[];
|
||||
pinned: Child[];
|
||||
}
|
||||
const selectSidebarFilter = createSelector(
|
||||
selectActiveWorkspaceMeta,
|
||||
activeWorkspaceMeta => activeWorkspaceMeta ? activeWorkspaceMeta.sidebarFilter : '',
|
||||
);
|
||||
const selectPinnedRequests = createSelector(selectEntitiesLists, entities => {
|
||||
const pinned: Record<string, boolean> = {};
|
||||
const requests = [...entities.requests, ...entities.grpcRequests, ...entities.webSocketRequests];
|
||||
const requestMetas = [...entities.requestMetas, ...entities.grpcRequestMetas];
|
||||
|
||||
// Default all to unpinned
|
||||
for (const request of requests) {
|
||||
pinned[request._id] = false;
|
||||
}
|
||||
|
||||
// Update those that have metadata (not all do)
|
||||
for (const meta of requestMetas) {
|
||||
pinned[meta.parentId] = meta.pinned;
|
||||
}
|
||||
|
||||
return pinned;
|
||||
});
|
||||
const selectCollapsedRequestGroups = createSelector(
|
||||
selectEntitiesLists,
|
||||
entities => {
|
||||
const collapsed: Record<string, boolean> = {};
|
||||
|
||||
// Default all to collapsed
|
||||
for (const requestGroup of entities.requestGroups) {
|
||||
collapsed[requestGroup._id] = true;
|
||||
}
|
||||
|
||||
// Update those that have metadata (not all do)
|
||||
for (const meta of entities.requestGroupMetas) {
|
||||
collapsed[meta.parentId] = meta.collapsed;
|
||||
}
|
||||
|
||||
return collapsed;
|
||||
});
|
||||
export const selectSidebarChildren = createSelector(
|
||||
selectCollapsedRequestGroups,
|
||||
selectPinnedRequests,
|
||||
selectActiveWorkspace,
|
||||
selectEntitiesChildrenMap,
|
||||
selectSidebarFilter,
|
||||
(collapsed, pinned, activeWorkspace, childrenMap, sidebarFilter): SidebarChildren => {
|
||||
if (!activeWorkspace) {
|
||||
return { all: [], pinned: [] };
|
||||
}
|
||||
|
||||
function next(parentId: string, pinnedChildren: Child[]) {
|
||||
const children: SidebarModel[] = (childrenMap[parentId] || [])
|
||||
.filter(shouldShowInSidebar)
|
||||
.sort(sortByMetaKeyOrId);
|
||||
|
||||
if (children.length > 0) {
|
||||
return children.map(c => {
|
||||
const child: Child = {
|
||||
doc: c,
|
||||
hidden: false,
|
||||
collapsed: !!collapsed[c._id],
|
||||
pinned: !!pinned[c._id],
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (child.pinned) {
|
||||
pinnedChildren.push(child);
|
||||
}
|
||||
|
||||
// Don't add children of requests
|
||||
child.children = shouldIgnoreChildrenOf(c) ? [] : next(c._id, pinnedChildren);
|
||||
return child;
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function matchChildren(children: Child[], parentNames: string[] = []) {
|
||||
// Bail early if no filter defined
|
||||
if (!sidebarFilter) {
|
||||
return children;
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
// Gather all parents so we can match them too
|
||||
matchChildren(child.children, [...parentNames, child.doc.name]);
|
||||
const hasMatchedChildren = child.children.find(c => c.hidden === false);
|
||||
// Try to match request attributes
|
||||
const name = child.doc.name;
|
||||
const method = isGrpcRequest(child.doc) ? 'gRPC' : isRequest(child.doc) ? child.doc.method : '';
|
||||
const match = fuzzyMatchAll(sidebarFilter, [name, method, ...parentNames], {
|
||||
splitSpace: true,
|
||||
});
|
||||
// Update hidden state depending on whether it matched
|
||||
const matched = hasMatchedChildren || match;
|
||||
child.hidden = !matched;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
const pinnedChildren: Child[] = [];
|
||||
const childrenTree = next(activeWorkspace._id, pinnedChildren);
|
||||
const matchedChildren = matchChildren(childrenTree);
|
||||
|
||||
return {
|
||||
pinned: pinnedChildren,
|
||||
all: matchedChildren,
|
||||
};
|
||||
},
|
||||
(docs: BaseModel[]) => docs.filter(canSync).map((doc: BaseModel): StatusCandidate => ({
|
||||
key: doc._id,
|
||||
name: doc.name || '',
|
||||
document: doc,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -937,3 +937,9 @@ export const deleteClientCertificateAction: ActionFunction = async ({ request })
|
||||
await models.clientCertificate.remove(certificateId);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const updateSettingsAction: ActionFunction = async ({ request }) => {
|
||||
const patch = await request.json();
|
||||
await models.settings.patch(patch);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ServiceError, StatusObject } from '@grpc/grpc-js';
|
||||
import React, { FC, Fragment, useEffect, useState } from 'react';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { LoaderFunction, redirect, useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { ChangeBufferEvent, database as db } from '../../common/database';
|
||||
import { generateId } from '../../common/misc';
|
||||
import type { GrpcMethodInfo } from '../../main/ipc/grpc';
|
||||
import * as models from '../../models';
|
||||
import { GrpcRequest, isGrpcRequest, isGrpcRequestId } from '../../models/grpc-request';
|
||||
import { isGrpcRequest, isGrpcRequestId } from '../../models/grpc-request';
|
||||
import { getByParentId as getGrpcRequestMetaByParentId } from '../../models/grpc-request-meta';
|
||||
import { isEventStreamRequest, isRequest, isRequestId, Request } from '../../models/request';
|
||||
import { isEventStreamRequest, isRequest, isRequestId } from '../../models/request';
|
||||
import { getByParentId as getRequestMetaByParentId } from '../../models/request-meta';
|
||||
import { isWebSocketRequest, isWebSocketRequestId, WebSocketRequest } from '../../models/websocket-request';
|
||||
import { isWebSocketRequest, isWebSocketRequestId } from '../../models/websocket-request';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { EnvironmentsDropdown } from '../components/dropdowns/environments-dropdown';
|
||||
import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown';
|
||||
@@ -18,11 +18,10 @@ import { ErrorBoundary } from '../components/error-boundary';
|
||||
import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder';
|
||||
import { showModal, showPrompt } from '../components/modals';
|
||||
import { AskModal } from '../components/modals/ask-modal';
|
||||
import { CookiesModal, showCookiesModal } from '../components/modals/cookies-modal';
|
||||
import { CookiesModal } from '../components/modals/cookies-modal';
|
||||
import { GenerateCodeModal } from '../components/modals/generate-code-modal';
|
||||
import { PromptModal } from '../components/modals/prompt-modal';
|
||||
import { RequestSettingsModal } from '../components/modals/request-settings-modal';
|
||||
import { RequestSwitcherModal } from '../components/modals/request-switcher-modal';
|
||||
import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
|
||||
import { GrpcRequestPane } from '../components/panes/grpc-request-pane';
|
||||
import { GrpcResponsePane } from '../components/panes/grpc-response-pane';
|
||||
@@ -35,7 +34,7 @@ import { SidebarLayout } from '../components/sidebar-layout';
|
||||
import { RealtimeResponsePane } from '../components/websockets/realtime-response-pane';
|
||||
import { WebSocketRequestPane } from '../components/websockets/websocket-request-pane';
|
||||
import { useRequestMetaPatcher } from '../hooks/use-request';
|
||||
import { RequestLoaderData } from './request';
|
||||
import { GrpcRequestLoaderData, RequestLoaderData, WebSocketRequestLoaderData } from './request';
|
||||
import { RootLoaderData } from './root';
|
||||
import { WorkspaceLoaderData } from './workspace';
|
||||
export interface GrpcMessage {
|
||||
@@ -62,18 +61,38 @@ const INITIAL_GRPC_REQUEST_STATE = {
|
||||
error: undefined,
|
||||
methods: [],
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }) => {
|
||||
if (!params.requestId) {
|
||||
const { projectId, workspaceId, organizationId } = params;
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
invariant(projectId, 'Project ID is required');
|
||||
const activeWorkspace = await models.workspace.getById(workspaceId);
|
||||
invariant(activeWorkspace, 'Workspace not found');
|
||||
const activeWorkspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId);
|
||||
invariant(activeWorkspaceMeta, 'Workspace meta not found');
|
||||
const activeRequestId = activeWorkspaceMeta.activeRequestId;
|
||||
if (activeRequestId) {
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${activeRequestId}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export const Debug: FC = () => {
|
||||
const {
|
||||
activeWorkspace,
|
||||
activeWorkspaceMeta,
|
||||
activeEnvironment,
|
||||
grpcRequests,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request | GrpcRequest | WebSocketRequest, any> | undefined;
|
||||
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData | GrpcRequestLoaderData | WebSocketRequestLoaderData | undefined;
|
||||
const { activeRequest } = requestData || {};
|
||||
const requestFetcher = useFetcher();
|
||||
const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const [grpcStates, setGrpcStates] = useState<GrpcRequestState[]>([]);
|
||||
const [grpcStates, setGrpcStates] = useState<GrpcRequestState[]>(grpcRequests.map(r => ({ requestId: r._id, ...INITIAL_GRPC_REQUEST_STATE })));
|
||||
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
|
||||
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
|
||||
const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false);
|
||||
|
||||
const patchRequestMeta = useRequestMetaPatcher();
|
||||
useEffect(() => {
|
||||
db.onChange(async (changes: ChangeBufferEvent[]) => {
|
||||
@@ -85,15 +104,7 @@ export const Debug: FC = () => {
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const fn = async () => {
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
const children = await db.withDescendants(workspace);
|
||||
const grpcRequests = children.filter(d => isGrpcRequest(d));
|
||||
setGrpcStates(grpcRequests.map(r => ({ requestId: r._id, ...INITIAL_GRPC_REQUEST_STATE })));
|
||||
};
|
||||
fn();
|
||||
}, [workspaceId]);
|
||||
|
||||
const {
|
||||
settings,
|
||||
} = useRouteLoaderData('root') as RootLoaderData;
|
||||
@@ -133,6 +144,7 @@ export const Debug: FC = () => {
|
||||
useEffect(() => window.main.on('grpc.status', (_, id, status) => {
|
||||
setGrpcStates(state => state.map(s => s.requestId === id ? { ...s, status } : s));
|
||||
}), []);
|
||||
|
||||
useDocBodyKeyboardShortcuts({
|
||||
request_togglePin:
|
||||
async () => {
|
||||
@@ -144,7 +156,7 @@ export const Debug: FC = () => {
|
||||
request_showSettings:
|
||||
() => {
|
||||
if (activeRequest) {
|
||||
showModal(RequestSettingsModal, { request: activeRequest });
|
||||
setIsRequestSettingsModalOpen(true);
|
||||
}
|
||||
},
|
||||
request_showDelete:
|
||||
@@ -211,23 +223,14 @@ export const Debug: FC = () => {
|
||||
}),
|
||||
});
|
||||
},
|
||||
// TODO: fix these
|
||||
request_showRecent:
|
||||
() => showModal(RequestSwitcherModal, {
|
||||
disableInput: true,
|
||||
maxRequests: 10,
|
||||
maxWorkspaces: 0,
|
||||
selectOnKeyup: true,
|
||||
title: 'Recent Requests',
|
||||
hideNeverActiveRequests: true,
|
||||
// Add an open delay so the dialog won't show for quick presses
|
||||
openDelay: 150,
|
||||
}),
|
||||
() => { },
|
||||
request_quickSwitch:
|
||||
() => showModal(RequestSwitcherModal),
|
||||
() => { },
|
||||
environment_showEditor:
|
||||
() => showModal(WorkspaceEnvironmentsEditModal),
|
||||
showCookiesEditor:
|
||||
() => showModal(CookiesModal),
|
||||
() => setEnvironmentModalOpen(true),
|
||||
showCookiesEditor: () => setIsCookieModalOpen(true),
|
||||
request_showGenerateCodeEditor:
|
||||
() => {
|
||||
if (activeRequest && isRequest(activeRequest)) {
|
||||
@@ -250,13 +253,22 @@ export const Debug: FC = () => {
|
||||
<EnvironmentsDropdown
|
||||
activeEnvironment={activeEnvironment}
|
||||
workspaceId={workspaceId}
|
||||
setEnvironmentModalOpen={setEnvironmentModalOpen}
|
||||
/>
|
||||
<button className="btn btn--super-compact" onClick={showCookiesModal}>
|
||||
<button className="btn btn--super-compact" onClick={() => setIsCookieModalOpen(true)}>
|
||||
<div className="sidebar__menu__thing">
|
||||
<span>Cookies</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{isEnvironmentModalOpen && (
|
||||
<WorkspaceEnvironmentsEditModal onHide={() => setEnvironmentModalOpen(false)} />)
|
||||
}
|
||||
{isCookieModalOpen && (
|
||||
<CookiesModal
|
||||
onHide={() => setIsCookieModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarFilter
|
||||
key={`${workspaceId}::filter`}
|
||||
@@ -284,6 +296,12 @@ export const Debug: FC = () => {
|
||||
setLoading={setLoading}
|
||||
/>)}
|
||||
{!requestId && <PlaceholderRequestPane />}
|
||||
{isRequestSettingsModalOpen && activeRequest && (
|
||||
<RequestSettingsModal
|
||||
request={activeRequest}
|
||||
onHide={() => setIsRequestSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
: null}
|
||||
renderPaneTwo={
|
||||
|
||||
@@ -7,37 +7,28 @@ import { AddKeyCombinationModal } from '../components/modals/add-key-combination
|
||||
import { AlertModal } from '../components/modals/alert-modal';
|
||||
import { AskModal } from '../components/modals/ask-modal';
|
||||
import { CodePromptModal } from '../components/modals/code-prompt-modal';
|
||||
import { CookieModifyModal } from '../components/modals/cookie-modify-modal';
|
||||
import { CookiesModal } from '../components/modals/cookies-modal';
|
||||
import { EnvironmentEditModal } from '../components/modals/environment-edit-modal';
|
||||
import { ErrorModal } from '../components/modals/error-modal';
|
||||
import { ExportRequestsModal } from '../components/modals/export-requests-modal';
|
||||
import { FilterHelpModal } from '../components/modals/filter-help-modal';
|
||||
import { GenerateCodeModal } from '../components/modals/generate-code-modal';
|
||||
import { GenerateConfigModal } from '../components/modals/generate-config-modal';
|
||||
import { LoginModal } from '../components/modals/login-modal';
|
||||
import { NunjucksModal } from '../components/modals/nunjucks-modal';
|
||||
import { PromptModal } from '../components/modals/prompt-modal';
|
||||
import { RequestGroupSettingsModal } from '../components/modals/request-group-settings-modal';
|
||||
import { RequestRenderErrorModal } from '../components/modals/request-render-error-modal';
|
||||
import { RequestSettingsModal } from '../components/modals/request-settings-modal';
|
||||
import { RequestSwitcherModal } from '../components/modals/request-switcher-modal';
|
||||
import { ResponseDebugModal } from '../components/modals/response-debug-modal';
|
||||
import { SelectModal } from '../components/modals/select-modal';
|
||||
import { SettingsModal } from '../components/modals/settings-modal';
|
||||
import { SyncBranchesModal } from '../components/modals/sync-branches-modal';
|
||||
import { SyncDeleteModal } from '../components/modals/sync-delete-modal';
|
||||
import { SyncHistoryModal } from '../components/modals/sync-history-modal';
|
||||
import { SyncMergeModal } from '../components/modals/sync-merge-modal';
|
||||
import { SyncStagingModal } from '../components/modals/sync-staging-modal';
|
||||
import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
|
||||
import { WrapperModal } from '../components/modals/wrapper-modal';
|
||||
import { useVCS } from '../hooks/use-vcs';
|
||||
import { WorkspaceLoaderData } from './workspace';
|
||||
|
||||
const Modals: FC = () => {
|
||||
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
|
||||
const { activeWorkspace, activeEnvironment, activeCookieJar } = workspaceData || {};
|
||||
const { activeWorkspace, activeEnvironment } = workspaceData || {};
|
||||
const vcs = useVCS({
|
||||
workspaceId: activeWorkspace?._id,
|
||||
});
|
||||
@@ -71,38 +62,13 @@ const Modals: FC = () => {
|
||||
<CodePromptModal
|
||||
ref={instance => registerModal(instance, 'CodePromptModal')}
|
||||
/>
|
||||
<RequestSettingsModal
|
||||
ref={instance => registerModal(instance, 'RequestSettingsModal')}
|
||||
/>
|
||||
<RequestGroupSettingsModal
|
||||
ref={instance =>
|
||||
registerModal(instance, 'RequestGroupSettingsModal')
|
||||
}
|
||||
/>
|
||||
|
||||
{activeWorkspace ? (
|
||||
<>
|
||||
{/* TODO: Figure out why cookieJar is sometimes null */}
|
||||
{activeCookieJar ? (
|
||||
<>
|
||||
<CookiesModal
|
||||
ref={instance => registerModal(instance, 'CookiesModal')}
|
||||
/>
|
||||
<CookieModifyModal
|
||||
ref={instance =>
|
||||
registerModal(instance, 'CookieModifyModal')
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<NunjucksModal
|
||||
ref={instance => registerModal(instance, 'NunjucksModal')}
|
||||
workspace={activeWorkspace}
|
||||
/>
|
||||
|
||||
<RequestSwitcherModal
|
||||
ref={instance => registerModal(instance, 'RequestSwitcherModal')}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -131,14 +97,6 @@ const Modals: FC = () => {
|
||||
<SyncMergeModal
|
||||
ref={instance => registerModal(instance, 'SyncMergeModal')}
|
||||
/>
|
||||
<SyncBranchesModal
|
||||
ref={instance => registerModal(instance, 'SyncBranchesModal')}
|
||||
vcs={vcs}
|
||||
/>
|
||||
<SyncDeleteModal
|
||||
ref={instance => registerModal(instance, 'SyncDeleteModal')}
|
||||
vcs={vcs}
|
||||
/>
|
||||
<SyncHistoryModal
|
||||
ref={instance => registerModal(instance, 'SyncHistoryModal')}
|
||||
vcs={vcs}
|
||||
@@ -146,18 +104,9 @@ const Modals: FC = () => {
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
<WorkspaceEnvironmentsEditModal
|
||||
ref={instance =>
|
||||
registerModal(instance, 'WorkspaceEnvironmentsEditModal')
|
||||
}
|
||||
/>
|
||||
|
||||
<AddKeyCombinationModal
|
||||
ref={instance => registerModal(instance, 'AddKeyCombinationModal')}
|
||||
/>
|
||||
<ExportRequestsModal
|
||||
ref={instance => registerModal(instance, 'ExportRequestsModal')}
|
||||
/>
|
||||
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ActionFunction } from 'react-router-dom';
|
||||
|
||||
import * as models from '../../models';
|
||||
import { RequestGroup } from '../../models/request-group';
|
||||
import { RequestGroupMeta } from '../../models/request-group-meta';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
|
||||
export const createRequestGroupAction: ActionFunction = async ({ request, params }) => {
|
||||
@@ -18,9 +19,7 @@ export const updateRequestGroupAction: ActionFunction = async ({ request }) => {
|
||||
invariant(typeof patch._id === 'string', 'Request Group ID is required');
|
||||
const reqGroup = await models.requestGroup.getById(patch._id);
|
||||
invariant(reqGroup, 'Request Group not found');
|
||||
if (name !== null) {
|
||||
models.requestGroup.update(reqGroup, patch);
|
||||
}
|
||||
models.requestGroup.update(reqGroup, patch);
|
||||
return null;
|
||||
};
|
||||
export const deleteRequestGroupAction: ActionFunction = async ({ request }) => {
|
||||
@@ -32,3 +31,16 @@ export const deleteRequestGroupAction: ActionFunction = async ({ request }) => {
|
||||
models.requestGroup.remove(requestGroup);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const updateRequestGroupMetaAction: ActionFunction = async ({ request, params }) => {
|
||||
const { requestGroupId } = params;
|
||||
invariant(typeof requestGroupId === 'string', 'Request Group ID is required');
|
||||
const patch = await request.json() as Partial<RequestGroupMeta>;
|
||||
const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroupId);
|
||||
if (requestGroupMeta) {
|
||||
models.requestGroupMeta.update(requestGroupMeta, patch);
|
||||
return null;
|
||||
}
|
||||
models.requestGroupMeta.create({ parentId: requestGroupId, collapsed: false });
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,22 +1,46 @@
|
||||
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom';
|
||||
|
||||
import { CONTENT_TYPE_EVENT_STREAM, CONTENT_TYPE_GRAPHQL, CONTENT_TYPE_JSON, METHOD_GET, METHOD_POST } from '../../common/constants';
|
||||
import { delay } from '../../common/misc';
|
||||
import * as models from '../../models';
|
||||
import { BaseModel } from '../../models';
|
||||
import { CookieJar } from '../../models/cookie-jar';
|
||||
import { GrpcRequest, isGrpcRequestId } from '../../models/grpc-request';
|
||||
import { GrpcRequestMeta } from '../../models/grpc-request-meta';
|
||||
import * as requestOperations from '../../models/helpers/request-operations';
|
||||
import { isRequest, Request } from '../../models/request';
|
||||
import { isEventStreamRequest, isRequest, Request, RequestAuthentication, RequestHeader } from '../../models/request';
|
||||
import { RequestMeta } from '../../models/request-meta';
|
||||
import { WebSocketRequest } from '../../models/websocket-request';
|
||||
import { RequestVersion } from '../../models/request-version';
|
||||
import { Response } from '../../models/response';
|
||||
import { isWebSocketRequestId, WebSocketRequest } from '../../models/websocket-request';
|
||||
import { WebSocketResponse } from '../../models/websocket-response';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { SegmentEvent } from '../analytics';
|
||||
import { updateMimeType } from '../components/dropdowns/content-type-dropdown';
|
||||
|
||||
export interface RequestLoaderData<A, B> {
|
||||
activeRequest: A;
|
||||
activeRequestMeta: B;
|
||||
export interface WebSocketRequestLoaderData {
|
||||
activeRequest: WebSocketRequest;
|
||||
activeRequestMeta: RequestMeta;
|
||||
activeResponse: WebSocketResponse | null;
|
||||
responses: WebSocketResponse[];
|
||||
requestVersions: RequestVersion[];
|
||||
}
|
||||
export const loader: LoaderFunction = async ({ params }): Promise<RequestLoaderData<Request | WebSocketRequest | GrpcRequest, RequestMeta | GrpcRequestMeta>> => {
|
||||
export interface GrpcRequestLoaderData {
|
||||
activeRequest: GrpcRequest;
|
||||
activeRequestMeta: GrpcRequestMeta;
|
||||
activeResponse: null;
|
||||
responses: [];
|
||||
requestVersions: RequestVersion[];
|
||||
}
|
||||
export interface RequestLoaderData {
|
||||
activeRequest: Request;
|
||||
activeRequestMeta: RequestMeta;
|
||||
activeResponse: Response | null;
|
||||
responses: Response[];
|
||||
requestVersions: RequestVersion[];
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }): Promise<RequestLoaderData | WebSocketRequestLoaderData | GrpcRequestLoaderData> => {
|
||||
const { requestId, workspaceId } = params;
|
||||
invariant(requestId, 'Request ID is required');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
@@ -25,18 +49,36 @@ export const loader: LoaderFunction = async ({ params }): Promise<RequestLoaderD
|
||||
const activeWorkspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
invariant(activeWorkspaceMeta, 'Active workspace meta not found');
|
||||
// NOTE: loaders shouldnt mutate data, this should be moved somewhere else
|
||||
models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: requestId });
|
||||
await models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: requestId });
|
||||
if (isGrpcRequestId(requestId)) {
|
||||
return {
|
||||
activeRequest,
|
||||
activeRequestMeta: await models.grpcRequestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() }),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
activeRequest,
|
||||
activeRequestMeta: await models.requestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() }),
|
||||
};
|
||||
activeResponse: null,
|
||||
responses: [],
|
||||
requestVersions: [],
|
||||
} as GrpcRequestLoaderData;
|
||||
}
|
||||
const activeRequestMeta = await models.requestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() });
|
||||
invariant(activeRequestMeta, 'Request meta not found');
|
||||
const { filterResponsesByEnv } = await models.settings.getOrCreate();
|
||||
|
||||
const responseModelName = isWebSocketRequestId(requestId) ? 'webSocketResponse' : 'response';
|
||||
const activeResponse = activeRequestMeta.activeResponseId
|
||||
? await models[responseModelName].getById(activeRequestMeta.activeResponseId)
|
||||
: await models[responseModelName].getLatestForRequest(requestId, activeWorkspaceMeta.activeEnvironmentId);
|
||||
const allResponses = await models[responseModelName].findByParentId(requestId) as (Response | WebSocketResponse)[];
|
||||
const filteredResponses = allResponses
|
||||
.filter((r: Response | WebSocketResponse) => r.environmentId === activeWorkspaceMeta.activeEnvironmentId);
|
||||
const responses = (filterResponsesByEnv ? filteredResponses : allResponses)
|
||||
.sort((a: BaseModel, b: BaseModel) => (a.created > b.created ? -1 : 1));
|
||||
return {
|
||||
activeRequest,
|
||||
activeRequestMeta,
|
||||
activeResponse,
|
||||
responses,
|
||||
requestVersions: await models.requestVersion.findByParentId(requestId),
|
||||
} as RequestLoaderData | WebSocketRequestLoaderData;
|
||||
};
|
||||
|
||||
export const createRequestAction: ActionFunction = async ({ request, params }) => {
|
||||
@@ -124,15 +166,20 @@ export const updateRequestAction: ActionFunction = async ({ request, params }) =
|
||||
|
||||
export const deleteRequestAction: ActionFunction = async ({ request, params }) => {
|
||||
const { organizationId, projectId, workspaceId } = params;
|
||||
|
||||
invariant(typeof workspaceId === 'string', 'Workspace ID is required');
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id') as string;
|
||||
const req = await requestOperations.getById(id);
|
||||
invariant(req, 'Request not found');
|
||||
models.stats.incrementDeletedRequests();
|
||||
requestOperations.remove(req);
|
||||
// TODO: redirect only if we are deleting the active request
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`);
|
||||
await requestOperations.remove(req);
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
invariant(workspaceMeta, 'Workspace meta not found');
|
||||
if (workspaceMeta.activeRequestId === id) {
|
||||
await models.workspaceMeta.updateByParentId(workspaceId, { activeRequestId: null });
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const duplicateRequestAction: ActionFunction = async ({ request, params }) => {
|
||||
@@ -170,6 +217,92 @@ export const updateRequestMetaAction: ActionFunction = async ({ request, params
|
||||
await models.requestMeta.updateOrCreateByParentId(requestId, patch);
|
||||
return null;
|
||||
};
|
||||
export interface ConnectActionParams {
|
||||
url: string;
|
||||
headers: RequestHeader[];
|
||||
authentication: RequestAuthentication;
|
||||
cookieJar: CookieJar;
|
||||
}
|
||||
export const connectAction: ActionFunction = async ({ request, params }) => {
|
||||
const { requestId, workspaceId } = params;
|
||||
invariant(typeof requestId === 'string', 'Request ID is required');
|
||||
const req = await requestOperations.getById(requestId);
|
||||
invariant(req, 'Request not found');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
const rendered = await request.json() as ConnectActionParams;
|
||||
if (isWebSocketRequestId(requestId)) {
|
||||
window.main.webSocket.open({
|
||||
requestId,
|
||||
workspaceId,
|
||||
url: rendered.url,
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
cookieJar: rendered.cookieJar,
|
||||
});
|
||||
}
|
||||
if (isEventStreamRequest(req)) {
|
||||
window.main.curl.open({
|
||||
requestId,
|
||||
workspaceId,
|
||||
url: rendered.url,
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
cookieJar: rendered.cookieJar,
|
||||
});
|
||||
}
|
||||
// TODO: remove hack, show loading and reload after connection create response and set activeResponseId
|
||||
await delay(2000);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const deleteAllResponsesAction: ActionFunction = async ({ params }) => {
|
||||
const { workspaceId, requestId } = params;
|
||||
invariant(typeof requestId === 'string', 'Request ID is required');
|
||||
const req = await requestOperations.getById(requestId);
|
||||
invariant(req, 'Request not found');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
invariant(workspaceMeta, 'Active workspace meta not found');
|
||||
if (isWebSocketRequestId(requestId)) {
|
||||
await models.webSocketResponse.removeForRequest(requestId, workspaceMeta.activeEnvironmentId);
|
||||
} else {
|
||||
await models.response.removeForRequest(requestId, workspaceMeta.activeEnvironmentId);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const deleteResponseAction: ActionFunction = async ({ request, params }) => {
|
||||
const { workspaceId, requestId } = params;
|
||||
invariant(typeof requestId === 'string', 'Request ID is required');
|
||||
const req = await requestOperations.getById(requestId);
|
||||
invariant(req, 'Request not found');
|
||||
const { responseId } = await request.json();
|
||||
invariant(typeof responseId === 'string', 'Response ID is required');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
invariant(workspaceMeta, 'Active workspace meta not found');
|
||||
if (isWebSocketRequestId(requestId)) {
|
||||
const res = await models.webSocketResponse.getById(responseId);
|
||||
invariant(res, 'Response not found');
|
||||
await models.webSocketResponse.remove(res);
|
||||
const response = await models.webSocketResponse.getLatestForRequest(requestId, workspaceMeta.activeEnvironmentId);
|
||||
if (response?.requestVersionId) {
|
||||
await models.requestVersion.restore(response.requestVersionId);
|
||||
}
|
||||
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null });
|
||||
} else {
|
||||
const res = await models.response.getById(responseId);
|
||||
invariant(res, 'Response not found');
|
||||
await models.response.remove(res);
|
||||
const response = await models.response.getLatestForRequest(requestId, workspaceMeta.activeEnvironmentId);
|
||||
if (response?.requestVersionId) {
|
||||
await models.requestVersion.restore(response.requestVersionId);
|
||||
}
|
||||
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null });
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// const RequestRoute = () => {
|
||||
// const { requestId } = useParams() as { requestId: string };
|
||||
|
||||
@@ -47,6 +47,7 @@ import { AppHooks } from '../containers/app-hooks';
|
||||
import { AIProvider } from '../context/app/ai-context';
|
||||
import withDragDropContext from '../context/app/drag-drop-context';
|
||||
import { NunjucksEnabledProvider } from '../context/nunjucks/nunjucks-enabled-context';
|
||||
import { useSettingsPatcher } from '../hooks/use-request';
|
||||
import Modals from './modals';
|
||||
import { WorkspaceLoaderData } from './workspace';
|
||||
|
||||
@@ -101,6 +102,7 @@ const Root = () => {
|
||||
const { revalidate } = useRevalidator();
|
||||
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | null;
|
||||
const [importUri, setImportUri] = useState('');
|
||||
const patchSettings = useSettingsPatcher();
|
||||
|
||||
useEffect(() => {
|
||||
onLoginLogout(() => {
|
||||
@@ -199,10 +201,7 @@ const Root = () => {
|
||||
'0.0.1',
|
||||
mainJsContent
|
||||
);
|
||||
const settings = await models.settings.getOrCreate();
|
||||
await models.settings.update(settings, {
|
||||
theme: parsedTheme.name,
|
||||
});
|
||||
patchSettings({ theme: parsedTheme.name });
|
||||
await reloadPlugins();
|
||||
await setTheme(parsedTheme.name);
|
||||
showModal(SettingsModal, { tab: TAB_INDEX_THEMES });
|
||||
@@ -250,7 +249,7 @@ const Root = () => {
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
}, [patchSettings]);
|
||||
|
||||
const { organizationId } = useParams() as {
|
||||
organizationId: string;
|
||||
|
||||
@@ -8,9 +8,13 @@ import { ClientCertificate } from '../../models/client-certificate';
|
||||
import { CookieJar } from '../../models/cookie-jar';
|
||||
import { Environment } from '../../models/environment';
|
||||
import { GitRepository } from '../../models/git-repository';
|
||||
import { GrpcRequest } from '../../models/grpc-request';
|
||||
import { sortProjects } from '../../models/helpers/project';
|
||||
import { DEFAULT_ORGANIZATION_ID } from '../../models/organization';
|
||||
import { isRemoteProject, Project } from '../../models/project';
|
||||
import { Request } from '../../models/request';
|
||||
import { RequestGroup } from '../../models/request-group';
|
||||
import { WebSocketRequest } from '../../models/websocket-request';
|
||||
import { Workspace } from '../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../models/workspace-meta';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
@@ -27,6 +31,15 @@ export interface WorkspaceLoaderData {
|
||||
clientCertificates: ClientCertificate[];
|
||||
caCertificate: CaCertificate | null;
|
||||
projects: Project[];
|
||||
requestTree: Child[];
|
||||
grpcRequests: GrpcRequest[];
|
||||
}
|
||||
export interface Child {
|
||||
doc: Request | GrpcRequest | WebSocketRequest | RequestGroup;
|
||||
children: Child[];
|
||||
collapsed: boolean;
|
||||
hidden: boolean;
|
||||
pinned: boolean;
|
||||
}
|
||||
|
||||
export const workspaceLoader: LoaderFunction = async ({
|
||||
@@ -67,6 +80,7 @@ export const workspaceLoader: LoaderFunction = async ({
|
||||
|
||||
const activeApiSpec = await models.apiSpec.getByParentId(workspaceId);
|
||||
const clientCertificates = await models.clientCertificate.findByParentId(workspaceId);
|
||||
|
||||
const allProjects = await models.project.all();
|
||||
|
||||
const organizationProjects =
|
||||
@@ -75,6 +89,31 @@ export const workspaceLoader: LoaderFunction = async ({
|
||||
: [activeProject];
|
||||
|
||||
const projects = sortProjects(organizationProjects);
|
||||
const requestMetas = await models.requestMeta.all();
|
||||
const grpcRequestMetas = await models.grpcRequestMeta.all();
|
||||
const metas = [...requestMetas, ...grpcRequestMetas];
|
||||
const folderMetas = (await models.requestGroupMeta.all());
|
||||
const grpcRequestList: GrpcRequest[] = [];
|
||||
const recurse = async ({ parentId }: { parentId: string }): Promise<Child[]> => {
|
||||
const folders = await models.requestGroup.findByParentId(parentId);
|
||||
const requests = await models.request.findByParentId(parentId);
|
||||
const webSocketRequests = await models.webSocketRequest.findByParentId(parentId);
|
||||
const grpcRequests = await models.grpcRequest.findByParentId(parentId);
|
||||
// TODO: remove this state hack when the grpc responses go somewhere else
|
||||
grpcRequests.map(r => grpcRequestList.push(r));
|
||||
|
||||
const childrenWithChildren = await Promise.all([...folders, ...requests, ...webSocketRequests, ...grpcRequests].map(async doc => ({
|
||||
doc,
|
||||
pinned: metas.find(m => m.parentId === doc._id)?.pinned || false,
|
||||
collapsed: folderMetas.find(m => m.parentId === doc._id)?.collapsed || false,
|
||||
hidden: false,
|
||||
children: await recurse({ parentId: doc._id }),
|
||||
})));
|
||||
return childrenWithChildren;
|
||||
};
|
||||
const requestTree = await recurse({ parentId: activeWorkspace._id });
|
||||
|
||||
const grpcRequests = grpcRequestList;
|
||||
return {
|
||||
activeWorkspace,
|
||||
activeProject,
|
||||
@@ -88,6 +127,8 @@ export const workspaceLoader: LoaderFunction = async ({
|
||||
clientCertificates,
|
||||
caCertificate: await models.caCertificate.findByParentId(workspaceId),
|
||||
projects,
|
||||
requestTree,
|
||||
grpcRequests,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user