redux->remix workspace route (#6191)

* checkpoint

* workspace meta

* select environment

* fix sidebar layout

* clean up

* absorb apispec into workspace route

* client certs

* remove redux tests

* update data docs

* remove active project

* revert selector change

* fix selector

* remove activeworkspace

* remove activity
This commit is contained in:
Jack Kavanagh
2023-07-25 16:15:00 +02:00
committed by GitHub
parent c72eb2d373
commit 7886fdb350
42 changed files with 316 additions and 1196 deletions

View File

@@ -45,8 +45,8 @@ There are a few notable directories inside it:
Insomnia stores data in a few places:
- A local in-memory NeDB database stores data for data models (requests, folder, workspaces, etc.).
- A local Redux store contains an in-memory copy of all database entities.
- Multiple React Context stores, defined in `/src/ui/context`.
- localstorage
- a fake localstorage api that writes to file and is used for window sizing
> Note: NeDB is officially unmaintained (even for critical security bugs) and was last published in February 2016. Due to this, we hope to move away from it, however doing so is tricky because of how deeply tied it is to our architecture.

View File

@@ -1,7 +1,6 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { ACTIVITY_DEBUG } from '../common/constants';
import * as models from '../models';
import { Request } from '../models/request';
import { RequestMeta } from '../models/request-meta';
@@ -21,7 +20,6 @@ export const createMockStoreWithRequest = async ({ requestPatch, requestMetaPatc
const store = mockStore(await reduxStateForTest({
activeProjectId: projectId,
activeWorkspaceId: workspaceId,
activeActivity: ACTIVITY_DEBUG,
}));
return {

View File

@@ -1,4 +1,3 @@
import { ACTIVITY_HOME } from '../common/constants';
import { database } from '../common/database';
import { DEFAULT_PROJECT_ID, type } from '../models/project';
import { RootState } from '../ui/redux/modules';
@@ -36,7 +35,6 @@ export const reduxStateForTest = async (global: Partial<GlobalState> = {}): Prom
entities: entities.reducer(entities.initialEntitiesState, entities.initializeWith(allDocs)),
global: {
activeWorkspaceId: null,
activeActivity: ACTIVITY_HOME,
activeProjectId: DEFAULT_PROJECT_ID,
dashboardSortOrder: 'modified-desc',
isLoggedIn: false,

View File

@@ -3,7 +3,7 @@ import type { ApiSpec } from '../api-spec';
import * as models from '../index';
import { isDesign, Workspace } from '../workspace';
export async function rename(name: string, workspace: Workspace, apiSpec?: ApiSpec) {
export async function rename(name: string, workspace: Workspace, apiSpec?: ApiSpec | null) {
if (isDesign(workspace) && apiSpec) {
await models.apiSpec.update(apiSpec, {
fileName: name,

View File

@@ -1,12 +1,10 @@
import classNames from 'classnames';
import React, { FC, Fragment, ReactNode } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { selectIsLoggedIn } from '../redux/selectors';
import * as session from '../../account/session';
import { GitHubStarsButton } from './github-stars-button';
import { InsomniaAILogo } from './insomnia-icon';
const LogoWrapper = styled.div({
display: 'flex',
justifyContent: 'center',
@@ -75,8 +73,6 @@ export const AppHeader: FC<AppHeaderProps> = ({
gridCenter,
gridRight,
}) => {
const isLoggedIn = useSelector(selectIsLoggedIn);
return (
<Header
gridLeft={(
@@ -84,7 +80,7 @@ export const AppHeader: FC<AppHeaderProps> = ({
<LogoWrapper>
<InsomniaAILogo />
</LogoWrapper>
{!isLoggedIn ? <GitHubStarsButton /> : null}
{!session.isLoggedIn() ? <GitHubStarsButton /> : null}
</Fragment>
)}
gridCenter={gridCenter}

View File

@@ -1,5 +1,6 @@
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { toKebabCase } from '../../../common/misc';
import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';
@@ -11,11 +12,11 @@ import { getRequestGroupActions } from '../../../plugins';
import * as pluginContexts from '../../../plugins/context/index';
import { createRequest, CreateRequestType } from '../../hooks/create-request';
import { createRequestGroup } from '../../hooks/create-request-group';
import { selectActiveEnvironment, selectActiveProject, selectActiveWorkspace, selectHotKeyRegistry } from '../../redux/selectors';
import { selectHotKeyRegistry } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, type DropdownProps, DropdownSection, ItemContent } from '../base/dropdown';
import { showError, showModal, showPrompt } from '../modals';
import { EnvironmentEditModal } from '../modals/environment-edit-modal';
interface Props extends Partial<DropdownProps> {
requestGroup: RequestGroup;
handleShowSettings: (requestGroup: RequestGroup) => any;
@@ -30,24 +31,24 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
handleShowSettings,
...other
}, ref) => {
const {
activeWorkspace,
activeEnvironment,
activeProject,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
const [actionPlugins, setActionPlugins] = useState<RequestGroupAction[]>([]);
const [loadingActions, setLoadingActions] = useState<Record<string, boolean>>({});
const dropdownRef = useRef<DropdownHandle>(null);
const activeProject = useSelector(selectActiveProject);
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeWorkspaceId = activeWorkspace?._id;
const create = useCallback((requestType: CreateRequestType) => {
if (activeWorkspaceId) {
if (activeWorkspace._id) {
createRequest({
parentId: requestGroup._id,
requestType, workspaceId: activeWorkspaceId,
requestType, workspaceId: activeWorkspace._id,
});
}
}, [activeWorkspaceId, requestGroup._id]);
}, [activeWorkspace._id, requestGroup._id]);
useImperativeHandle(ref, () => ({
show: () => {
@@ -102,12 +103,11 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
setLoadingActions({ ...loadingActions, [label]: true });
try {
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
const context = {
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
...pluginContexts.data.init(activeProject._id),
...(pluginContexts.store.init(plugin) as Record<string, any>),
...(pluginContexts.network.init(activeEnvironmentId) as Record<string, any>),
...(pluginContexts.network.init(activeEnvironment._id) as Record<string, any>),
};
const requests = await models.request.findByParentId(requestGroup._id);
requests.sort((a, b) => a.metaSortKey - b.metaSortKey);

View File

@@ -1,6 +1,7 @@
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 { decompressObject } from '../../../common/misc';
import * as models from '../../../models/index';
@@ -9,7 +10,8 @@ import { Response } from '../../../models/response';
import { WebSocketRequest } from '../../../models/websocket-request';
import { isWebSocketResponse, WebSocketResponse } from '../../../models/websocket-response';
import { updateRequestMetaByParentId } from '../../hooks/create-request';
import { selectActiveEnvironment, selectActiveRequest, selectActiveRequestResponses, selectRequestVersions } from '../../redux/selectors';
import { selectActiveRequest, selectActiveRequestResponses, selectRequestVersions } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { useDocBodyKeyboardShortcuts } from '../keydown-binder';
import { SizeTag } from '../tags/size-tag';
@@ -17,7 +19,6 @@ 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;
@@ -30,7 +31,9 @@ export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSo
requestId,
}: Props<GenericResponse>) => {
const dropdownRef = useRef<DropdownHandle>(null);
const activeEnvironment = useSelector(selectActiveEnvironment);
const {
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const responses = useSelector(selectActiveRequestResponses) as GenericResponse[];
const activeRequest = useSelector(selectActiveRequest);
const requestVersions = useSelector(selectRequestVersions);
@@ -56,12 +59,11 @@ export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSo
}, []);
const handleDeleteResponses = useCallback(async () => {
const environmentId = activeEnvironment ? activeEnvironment._id : null;
if (isWebSocketResponse(activeResponse)) {
window.main.webSocket.closeAll();
await models.webSocketResponse.removeForRequest(requestId, environmentId);
await models.webSocketResponse.removeForRequest(requestId, activeEnvironment._id);
} else {
await models.response.removeForRequest(requestId, environmentId);
await models.response.removeForRequest(requestId, activeEnvironment._id);
}
if (activeRequest && activeRequest._id === requestId) {
await updateRequestMetaByParentId(requestId, { activeResponseId: null });

View File

@@ -1,6 +1,7 @@
import classnames from 'classnames';
import React, { FC, Fragment, useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useNavigate, useParams, useRouteLoaderData } from 'react-router-dom';
import { useInterval, useMount } from 'react-use';
import * as session from '../../../account/session';
@@ -18,8 +19,8 @@ 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 { activateWorkspace } from '../../redux/modules/workspace';
import { selectActiveWorkspaceMeta, selectRemoteProjects, selectSyncItems } from '../../redux/selectors';
import { selectRemoteProjects, selectSyncItems } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { Link } from '../base/link';
import { HelpTooltip } from '../help-tooltip';
@@ -32,7 +33,6 @@ import { SyncHistoryModal } from '../modals/sync-history-modal';
import { SyncStagingModal } from '../modals/sync-staging-modal';
import { Button } from '../themed-button';
import { Tooltip } from '../tooltip';
// TODO: handle refetching logic in one place not here in a component
// Refresh dropdown periodically
@@ -79,10 +79,13 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
},
remoteBackendProjects: [],
});
const dispatch = useDispatch();
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
const navigate = useNavigate();
const remoteProjects = useSelector(selectRemoteProjects);
const syncItems = useSelector(selectSyncItems);
const workspaceMeta = useSelector(selectActiveWorkspaceMeta);
const {
activeWorkspaceMeta,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const refetchRemoteBranch = useCallback(async () => {
if (session.isLoggedIn()) {
try {
@@ -136,7 +139,7 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
try {
// NOTE pushes the first snapshot automatically
await pushSnapshotOnInitialize({ vcs, workspace, workspaceMeta, project });
await pushSnapshotOnInitialize({ vcs, workspace, workspaceMeta: activeWorkspaceMeta, project });
await refreshVCSAndRefetchRemote();
} catch (err) {
console.log('[sync_menu] Error refreshing sync state', err);
@@ -167,7 +170,7 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
const pulledIntoProject = await pullBackendProject({ vcs, backendProject, remoteProjects });
if (pulledIntoProject.project._id !== project._id) {
// If pulled into a different project, reactivate the workspace
dispatch(activateWorkspace({ workspaceId: workspace._id }));
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}`);
logCollectionMovedToProject(workspace, pulledIntoProject.project);
}
await refreshVCSAndRefetchRemote();

View File

@@ -1,10 +1,12 @@
import React, { FC, useCallback, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { isLoggedIn } from '../../../account/session';
import { database as db } from '../../../common/database';
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';
import { workspace } from '../../../models';
import { isRequest } from '../../../models/request';
import { isRequestGroup } from '../../../models/request-group';
import { isDesign, Workspace } from '../../../models/workspace';
@@ -12,20 +14,23 @@ import type { WorkspaceAction } from '../../../plugins';
import { getWorkspaceActions } from '../../../plugins';
import * as pluginContexts from '../../../plugins/context';
import { useAIContext } from '../../context/app/ai-context';
import { selectActiveApiSpec, selectActiveEnvironment, selectActiveProject, selectActiveWorkspace, selectActiveWorkspaceName, selectSettings } from '../../redux/selectors';
import { selectSettings } from '../../redux/selectors';
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 } from '../modals';
import { configGenerators, showGenerateConfigModal } from '../modals/generate-config-modal';
import { SettingsModal, TAB_INDEX_EXPORT } from '../modals/settings-modal';
import { WorkspaceSettingsModal } from '../modals/workspace-settings-modal';
export const WorkspaceDropdown: FC = () => {
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
const activeApiSpec = useSelector(selectActiveApiSpec);
const activeProject = useSelector(selectActiveProject);
const {
activeWorkspace,
activeEnvironment,
activeProject,
activeApiSpec,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const activeWorkspaceName = workspace.name;
const settings = useSelector(selectSettings);
const { hotKeyRegistry } = settings;
const [actionPlugins, setActionPlugins] = useState<WorkspaceAction[]>([]);
@@ -41,12 +46,11 @@ export const WorkspaceDropdown: FC = () => {
const handlePluginClick = useCallback(async ({ action, plugin, label }: WorkspaceAction, workspace: Workspace) => {
setLoadingActions({ ...loadingActions, [label]: true });
try {
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
const context = {
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
...pluginContexts.data.init(activeProject._id),
...(pluginContexts.store.init(plugin) as Record<string, any>),
...(pluginContexts.network.init(activeEnvironmentId) as Record<string, any>),
...(pluginContexts.network.init(activeEnvironment._id) as Record<string, any>),
};
const docs = await db.withDescendants(workspace);
@@ -94,11 +98,6 @@ export const WorkspaceDropdown: FC = () => {
});
}, [activeApiSpec]);
if (!activeWorkspace) {
console.error('warning: tried to render WorkspaceDropdown without an activeWorkspace');
return null;
}
return (
<Dropdown
aria-label="Workspace Dropdown"

View File

@@ -1,13 +1,13 @@
import clone from 'clone';
import { isValid } from 'date-fns';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { Cookie as ToughCookie } from 'tough-cookie';
import { cookieToString } from '../../../common/cookies';
import * as models from '../../../models';
import type { Cookie } from '../../../models/cookie-jar';
import { selectActiveCookieJar } 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';
@@ -24,7 +24,8 @@ export interface CookieModifyModalHandle {
export const CookieModifyModal = forwardRef<CookieModifyModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [cookie, setCookie] = useState<Cookie | null>(null);
const activeCookieJar = useSelector(selectActiveCookieJar);
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();

View File

@@ -1,11 +1,11 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { fuzzyMatch } from '../../../common/misc';
import * as models from '../../../models';
import type { Cookie } from '../../../models/cookie-jar';
import { useNunjucks } from '../../context/nunjucks/use-nunjucks';
import { selectActiveCookieJar } 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';
@@ -21,7 +21,7 @@ export const CookiesModal = forwardRef<CookiesModalHandle, ModalProps>((_, ref)
const { handleRender } = useNunjucks();
const [filter, setFilter] = useState<string>('');
const [visibleCookieIndexes, setVisibleCookieIndexes] = useState<number[] | null>(null);
const activeCookieJar = useSelector(selectActiveCookieJar);
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
useImperativeHandle(ref, () => ({
hide: () => {

View File

@@ -1,6 +1,7 @@
import classnames from 'classnames';
import React, { forwardRef, Fragment, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useNavigate, useParams, useRouteLoaderData } from 'react-router-dom';
import { METHOD_GRPC } from '../../../common/constants';
import { fuzzyMatchAll } from '../../../common/misc';
@@ -12,8 +13,8 @@ import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-
import { Workspace } from '../../../models/workspace';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
import { updateRequestMetaByParentId } from '../../hooks/create-request';
import { activateWorkspace } from '../../redux/modules/workspace';
import { selectActiveRequest, selectActiveWorkspace, selectActiveWorkspaceMeta, selectGrpcRequestMetas, selectRequestMetas, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
import { selectActiveRequest, selectGrpcRequestMetas, selectRequestMetas, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Highlight } from '../base/highlight';
import { Modal, ModalHandle, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
@@ -23,7 +24,6 @@ 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[];
@@ -67,10 +67,13 @@ export const RequestSwitcherModal = forwardRef<RequestSwitcherModalHandle, Modal
isModalVisible: true,
title: null,
});
const dispatch = useDispatch();
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
const navigate = useNavigate();
const activeRequest = useSelector(selectActiveRequest);
const workspace = useSelector(selectActiveWorkspace);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const {
activeWorkspace: workspace,
activeWorkspaceMeta,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
const requestMetas = useSelector(selectRequestMetas);
const grpcRequestMetas = useSelector(selectGrpcRequestMetas);
@@ -163,7 +166,7 @@ export const RequestSwitcherModal = forwardRef<RequestSwitcherModalHandle, Modal
}
const matchedWorkspaces = workspacesForActiveProject
.filter(w => w._id !== workspace?._id)
.filter(w => w._id !== workspace._id)
.filter(w => {
const name = w.name.toLowerCase();
const toMatch = searchString.toLowerCase();
@@ -180,7 +183,7 @@ export const RequestSwitcherModal = forwardRef<RequestSwitcherModalHandle, Modal
matchedRequests: matchedRequests.slice(0, maxRequests),
matchedWorkspaces: matchedWorkspaces.slice(0, maxWorkspaces),
}));
}, [state, getLastActiveRequestMap, workspaceRequestsAndRequestGroups, workspacesForActiveProject, activeRequest, isMatch, workspace?._id]);
}, [state, getLastActiveRequestMap, workspaceRequestsAndRequestGroups, workspacesForActiveProject, activeRequest, isMatch, workspace._id]);
useImperativeHandle(ref, () => ({
hide: () => {
@@ -206,21 +209,20 @@ export const RequestSwitcherModal = forwardRef<RequestSwitcherModalHandle, Modal
},
}), [handleChangeValue]);
const activateWorkspaceAndHide = useCallback((workspace?: Workspace) => {
const activateWorkspaceAndHide = useCallback((workspace: Workspace) => {
if (!workspace) {
return;
}
dispatch(activateWorkspace({ workspace }));
console.log(`[app] Activating workspace "${workspace.name}"`);
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}`);
modalRef.current?.hide();
}, [dispatch]);
}, [navigate, organizationId, projectId]);
const activateRequestAndHide = useCallback((request?: Request | WebSocketRequest | GrpcRequest) => {
if (!request) {
return;
}
if (activeWorkspaceMeta) {
models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: request._id });
}
models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: request._id });
updateRequestMetaByParentId(request._id, { lastActive: Date.now() });
modalRef.current?.hide();
}, [activeWorkspaceMeta]);

View File

@@ -1,11 +1,11 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { strings } from '../../../common/strings';
import { interceptAccessError } from '../../../sync/vcs/util';
import { VCS } from '../../../sync/vcs/vcs';
import { Button } from '../../components/themed-button';
import { selectActiveWorkspace } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalHeader } from '../base/modal-header';
@@ -31,6 +31,9 @@ export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }
error: '',
workspaceName: '',
});
const {
activeWorkspace,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
useImperativeHandle(ref, () => ({
hide: () => modalRef.current?.hide(),
@@ -43,7 +46,6 @@ export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }
modalRef.current?.show({ onHide });
},
}), []);
const activeWorkspace = useSelector(selectActiveWorkspace);
const onSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
try {

View File

@@ -1,13 +1,11 @@
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 { useSelector } from 'react-redux';
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
import { DraggableCollectionState, DroppableCollectionState, Item, ListState, useDraggableCollectionState, useDroppableCollectionState, useListState } from 'react-stately';
import { docsTemplateTags } from '../../../common/documentation';
import type { Environment } from '../../../models/environment';
import { selectActiveWorkspaceMeta } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown';
import { Editable } from '../base/editable';
@@ -30,12 +28,14 @@ interface SidebarListItemProps {
const SidebarListItem: FC<SidebarListItemProps> = ({
environment,
}: SidebarListItemProps) => {
const workspaceMeta = useSelector(selectActiveWorkspaceMeta);
const {
activeWorkspaceMeta,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
return (
<div
className={classnames({
'env-modal__sidebar-item': true,
'env-modal__sidebar-item--active': workspaceMeta?.activeEnvironmentId === environment._id,
'env-modal__sidebar-item--active': activeWorkspaceMeta.activeEnvironmentId === environment._id,
})}
>
{environment.color ? (

View File

@@ -1,9 +1,9 @@
import React, { FC, forwardRef, ReactNode, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useRevalidator } from 'react-router-dom';
import { useRouteLoaderData } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';
import { ACTIVITY_HOME } from '../../../common/constants';
import { database as db } from '../../../common/database';
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
import { CaCertificate } from '../../../models/ca-certificate';
@@ -12,8 +12,7 @@ import * as workspaceOperations from '../../../models/helpers/workspace-operatio
import * as models from '../../../models/index';
import { isRequest } from '../../../models/request';
import { invariant } from '../../../utils/invariant';
import { setActiveActivity } from '../../redux/modules/global';
import { selectActiveApiSpec, selectActiveWorkspace, selectActiveWorkspaceClientCertificates, selectActiveWorkspaceMeta, selectActiveWorkspaceName } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { FileInputButton } from '../base/file-input-button';
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
@@ -23,7 +22,6 @@ import { PanelContainer, TabItem, Tabs } from '../base/tabs';
import { HelpTooltip } from '../help-tooltip';
import { MarkdownEditor } from '../markdown-editor';
import { PasswordViewer } from '../viewers/password-viewer';
const CertificateFields = styled.div({
display: 'flex',
flexDirection: 'column',
@@ -89,12 +87,16 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
});
const { revalidate } = useRevalidator();
const workspace = useSelector(selectActiveWorkspace);
const apiSpec = useSelector(selectActiveApiSpec);
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
const clientCertificates = useSelector(selectActiveWorkspaceClientCertificates);
const workspaceMeta = useSelector(selectActiveWorkspaceMeta);
const {
activeWorkspace: workspace,
activeWorkspaceMeta,
activeApiSpec,
clientCertificates,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const activeWorkspaceName = workspace.name;
const navigate = useNavigate();
const { organizationId } = useParams() as { organizationId: string };
const [caCert, setCaCert] = useState<CaCertificate | null>(null);
useEffect(() => {
if (!workspace) {
@@ -107,13 +109,12 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
fn();
}, [workspace]);
const dispatch = useDispatch();
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: () => {
const hasDescription = !!workspace?.description;
const hasDescription = !!workspace.description;
setState(state => ({
...state,
showDescription: hasDescription,
@@ -122,7 +123,7 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
}));
modalRef.current?.show();
},
}), [workspace?.description]);
}), [workspace.description]);
const _handleClearAllResponses = async () => {
if (!workspace) {
@@ -157,7 +158,7 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
const certificate = {
host,
isPrivate,
parentId: workspace?._id,
parentId: workspace._id,
passphrase: passphrase || null,
disabled: false,
cert: crtPath || null,
@@ -174,7 +175,8 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
}
await models.stats.incrementDeletedRequestsForDescendents(workspace);
await models.workspace.remove(workspace);
dispatch(setActiveActivity(ACTIVITY_HOME));
navigate(`/organizations/${organizationId}`);
modalRef.current?.hide();
};
@@ -248,7 +250,7 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
type="text"
placeholder="Awesome API"
defaultValue={activeWorkspaceName}
onChange={event => workspaceOperations.rename(event.target.value, workspace, apiSpec)}
onChange={event => workspaceOperations.rename(event.target.value, workspace, activeApiSpec)}
/>
</label>
</div>
@@ -509,20 +511,20 @@ export const WorkspaceSettingsModal = forwardRef<WorkspaceSettingsModalHandle, M
>
<input
type="checkbox"
checked={Boolean(workspaceMeta?.gitRepositoryId)}
checked={Boolean(activeWorkspaceMeta?.gitRepositoryId)}
onChange={async () => {
if (workspaceMeta?.gitRepositoryId) {
await models.workspaceMeta.update(workspaceMeta, {
if (activeWorkspaceMeta?.gitRepositoryId) {
await models.workspaceMeta.update(activeWorkspaceMeta, {
gitRepositoryId: null,
});
} else {
invariant(workspaceMeta, 'Workspace meta not found');
invariant(activeWorkspaceMeta, 'Workspace meta not found');
const repo = await models.gitRepository.create({
uri: '',
});
await models.workspaceMeta.update(workspaceMeta, {
await models.workspaceMeta.update(activeWorkspaceMeta, {
gitRepositoryId: repo._id,
});
}

View File

@@ -1,5 +1,5 @@
import React, { FunctionComponent, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { useAsync } from 'react-use';
import styled from 'styled-components';
@@ -12,8 +12,8 @@ import * as models from '../../../models';
import type { GrpcRequest, GrpcRequestHeader } from '../../../models/grpc-request';
import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { selectActiveEnvironment } from '../../redux/selectors';
import { GrpcRequestState } from '../../routes/debug';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
import { GrpcSendButton } from '../buttons/grpc-send-button';
import { OneLineEditor } from '../codemirror/one-line-editor';
@@ -92,10 +92,12 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
const gitVersion = useGitVCSVersion();
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
const activeEnvironment = useSelector(selectActiveEnvironment);
const environmentId = activeEnvironment?._id || 'n/a';
const {
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const environmentId = activeEnvironment._id;
// Reset the response pane state when we switch requests, the environment gets modified, or the (Git|Sync)VCS version changes
const uniquenessKey = `${activeEnvironment?.modified}::${activeRequest?._id}::${gitVersion}::${activeRequestSyncVersion}`;
const uniquenessKey = `${activeEnvironment.modified}::${activeRequest?._id}::${gitVersion}::${activeRequestSyncVersion}`;
const method = methods.find(c => c.fullPath === activeRequest.protoMethodName);
const methodType = method?.type;
const handleRequestSend = async () => {

View File

@@ -1,14 +1,13 @@
import React, { FunctionComponent } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import type { GrpcRequest } from '../../../models/grpc-request';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { selectActiveEnvironment } from '../../redux/selectors';
import { GrpcRequestState } from '../../routes/debug';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { GrpcStatusTag } from '../tags/grpc-status-tag';
import { GrpcTabbedMessages } from '../viewers/grpc-tabbed-messages';
import { Pane, PaneBody, PaneHeader } from './pane';
interface Props {
activeRequest: GrpcRequest;
grpcState: GrpcRequestState;
@@ -17,9 +16,11 @@ interface Props {
export const GrpcResponsePane: FunctionComponent<Props> = ({ activeRequest, grpcState }) => {
const gitVersion = useGitVCSVersion();
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
const activeEnvironment = useSelector(selectActiveEnvironment);
const {
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
// Force re-render when we switch requests, the environment gets modified, or the (Git|Sync)VCS version changes
const uniquenessKey = `${activeEnvironment?.modified}::${activeRequest?._id}::${gitVersion}::${activeRequestSyncVersion}`;
const uniquenessKey = `${activeEnvironment.modified}::${activeRequest?._id}::${gitVersion}::${activeRequestSyncVersion}`;
const { responseMessages, status, error } = grpcState;
return (

View File

@@ -1,15 +1,15 @@
import React, { FC, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { createRequest } from '../../hooks/create-request';
import { selectActiveWorkspace, selectSettings } from '../../redux/selectors';
import { selectSettings } from '../../redux/selectors';
import { Hotkey } from '../hotkey';
import { Pane, PaneBody, PaneHeader } from './pane';
export const PlaceholderRequestPane: FC = () => {
const { hotKeyRegistry } = useSelector(selectSettings);
const workspaceId = useSelector(selectActiveWorkspace)?._id;
const { workspaceId } = useParams<{ workspaceId: string }>();
const createHttpRequest = useCallback(() => {
if (workspaceId) {
createRequest({

View File

@@ -1,5 +1,6 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import styled from 'styled-components';
import { version } from '../../../../package.json';
@@ -14,7 +15,8 @@ import type { Settings } from '../../../models/settings';
import { create, Workspace } from '../../../models/workspace';
import { deconstructQueryStringToParams, extractQueryStringFromUrl } from '../../../utils/url/querystring';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { selectActiveEnvironment, selectActiveRequestMeta } from '../../redux/selectors';
import { selectActiveRequestMeta } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
import { AuthDropdown } from '../dropdowns/auth-dropdown';
import { ContentTypeDropdown } from '../dropdowns/content-type-dropdown';
@@ -31,7 +33,6 @@ import { RenderedQueryString } from '../rendered-query-string';
import { RequestUrlBar, RequestUrlBarHandle } from '../request-url-bar';
import { Pane, PaneHeader } from './pane';
import { PlaceholderRequestPane } from './placeholder-request-pane';
const HeaderContainer = styled.div({
display: 'flex',
flexDirection: 'column',
@@ -268,7 +269,10 @@ export const RequestPane: FC<Props> = ({
}, [request]);
const gitVersion = useGitVCSVersion();
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
const activeEnvironment = useSelector(selectActiveEnvironment);
const {
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const activeRequestMeta = useSelector(selectActiveRequestMeta);
// Force re-render when we switch requests, the environment gets modified, or the (Git|Sync)VCS version changes
const uniqueKey = `${activeEnvironment?.modified}::${request?._id}::${gitVersion}::${activeRequestSyncVersion}::${activeRequestMeta?.activeResponseId}`;

View File

@@ -5,6 +5,7 @@ import { extension as mimeExtension } from 'mime-types';
import path from 'path';
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { useInterval } from 'react-use';
import styled from 'styled-components';
@@ -16,13 +17,13 @@ import { update } from '../../models/helpers/request-operations';
import { isEventStreamRequest, isRequest, Request } from '../../models/request';
import * as network from '../../network/network';
import { convert } from '../../utils/importers/convert';
import { invariant } from '../../utils/invariant';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../utils/url/querystring';
import { SegmentEvent } from '../analytics';
import { updateRequestMetaByParentId } from '../hooks/create-request';
import { useReadyState } from '../hooks/use-ready-state';
import { useTimeoutWhen } from '../hooks/useTimeoutWhen';
import { selectActiveEnvironment, selectActiveRequest, selectActiveWorkspace, selectHotKeyRegistry, selectResponseDownloadPath, selectSettings } from '../redux/selectors';
import { selectActiveRequest, selectHotKeyRegistry, selectResponseDownloadPath, selectSettings } from '../redux/selectors';
import { WorkspaceLoaderData } from '../routes/workspace';
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from './base/dropdown';
import { OneLineEditor, OneLineEditorHandle } from './codemirror/one-line-editor';
import { MethodDropdown } from './dropdowns/method-dropdown';
@@ -61,10 +62,12 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
uniquenessKey,
setLoading,
}, ref) => {
const {
activeWorkspace,
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const downloadPath = useSelector(selectResponseDownloadPath);
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeRequest = useSelector(selectActiveRequest);
const settings = useSelector(selectSettings);
const methodDropdownRef = useRef<DropdownHandle>(null);
@@ -123,7 +126,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
});
setLoading(true);
try {
const responsePatch = await network.send(request._id, activeEnvironment?._id);
const responsePatch = await network.send(request._id, activeEnvironment._id);
const headers = responsePatch.headers || [];
const header = getContentDispositionHeader(headers);
const nameFromHeader = header ? contentDisposition.parse(header.value).parameters.filename : null;
@@ -189,7 +192,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
await updateRequestMetaByParentId(request._id, { activeResponseId: null });
setLoading(false);
}
}, [activeEnvironment?._id, request, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
}, [activeEnvironment._id, request, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
const handleSend = useCallback(async () => {
if (!request) {
@@ -207,7 +210,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
});
setLoading(true);
try {
const responsePatch = await network.send(request._id, activeEnvironment?._id);
const responsePatch = await network.send(request._id, activeEnvironment._id);
await models.response.create(responsePatch, settings.maxHistoryResponses);
} catch (err) {
if (err.type === 'render') {
@@ -232,7 +235,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
// Unset active response because we just made a new one
await updateRequestMetaByParentId(request._id, { activeResponseId: null });
setLoading(false);
}, [activeEnvironment?._id, request, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
}, [activeEnvironment._id, request, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
const send = useCallback(() => {
setCurrentTimeout(undefined);
@@ -242,8 +245,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
}
if (isEventStreamRequest(request)) {
const startListening = async () => {
invariant(activeWorkspace, 'activeWorkspace not found (remove with redux)');
const environmentId = activeEnvironment?._id;
const environmentId = activeEnvironment._id;
const workspaceId = activeWorkspace._id;
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
// Render any nunjucks tags in the url/headers/authentication settings/cookies
@@ -268,7 +270,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
return;
}
handleSend();
}, [activeEnvironment?._id, activeWorkspace, downloadPath, handleSend, request, sendThenSetFilePath]);
}, [activeEnvironment._id, activeWorkspace, downloadPath, handleSend, request, sendThenSetFilePath]);
useInterval(send, currentInterval ? currentInterval : null);
useTimeoutWhen(send, currentTimeout, !!currentTimeout);

View File

@@ -1,6 +1,7 @@
import React, { FC, Fragment, useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { useRouteLoaderData } from 'react-router-dom';
import { getProductName } from '../../../common/constants';
import { docsImportExport } from '../../../common/documentation';
@@ -8,7 +9,8 @@ import { exportAllToFile } from '../../../common/export';
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
import { strings } from '../../../common/strings';
import { isRequestGroup } from '../../../models/request-group';
import { selectActiveProjectName, selectActiveWorkspace, selectActiveWorkspaceName, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
import { selectWorkspaceRequestsAndRequestGroups, 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';
@@ -16,7 +18,6 @@ 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;
}
@@ -27,9 +28,11 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
projectId,
workspaceId,
} = useParams() as { organizationId: string; projectId: string; workspaceId?: string };
const projectName = useSelector(selectActiveProjectName) ?? getProductName();
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
const activeWorkspaceName = workspaceData?.activeWorkspace.name;
const projectName = workspaceData?.activeProject.name ?? getProductName();
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
const workspaceRequestsAndRequestGroups = useSelector(selectWorkspaceRequestsAndRequestGroups);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
@@ -61,7 +64,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
Your format isn't supported? <Link href={docsImportExport}>Add Your Own</Link>.
</p>
<div className="pad-top">
{activeWorkspace ?
{workspaceData?.activeWorkspace ?
(<Dropdown
aria-label='Export Data Dropdown'
triggerButton={
@@ -74,10 +77,10 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
aria-label="Choose Export Type"
title="Choose Export Type"
>
<DropdownItem aria-label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(activeWorkspace).singular}`}>
<DropdownItem aria-label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(workspaceData.activeWorkspace).singular}`}>
<ItemContent
icon="home"
label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(activeWorkspace).singular}`}
label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(workspaceData.activeWorkspace).singular}`}
onClick={showExportRequestsModal}
/>
</DropdownItem>

View File

@@ -1,15 +1,15 @@
import React, { FC, forwardRef, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import styled from 'styled-components';
import { COLLAPSE_SIDEBAR_REMS, DEFAULT_PANE_HEIGHT, DEFAULT_PANE_WIDTH, DEFAULT_SIDEBAR_WIDTH, MAX_PANE_HEIGHT, MAX_PANE_WIDTH, MAX_SIDEBAR_REMS, MIN_PANE_HEIGHT, MIN_PANE_WIDTH, MIN_SIDEBAR_REMS } from '../../common/constants';
import { debounce } from '../../common/misc';
import * as models from '../../models';
import { selectActiveWorkspaceMeta, selectSettings } from '../redux/selectors';
import { selectPaneHeight, selectPaneWidth, selectSidebarWidth } from '../redux/sidebar-selectors';
import { selectSettings } from '../redux/selectors';
import { WorkspaceLoaderData } from '../routes/workspace';
import { ErrorBoundary } from './error-boundary';
import { Sidebar } from './sidebar/sidebar';
const verticalStyles = {
'.sidebar': {
gridColumnStart: '2',
@@ -203,11 +203,8 @@ export const SidebarLayout: FC<Props> = ({
renderPageSidebar,
}) => {
const { forceVerticalLayout } = useSelector(selectSettings);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const reduxPaneHeight = useSelector(selectPaneHeight);
const reduxPaneWidth = useSelector(selectPaneWidth);
const reduxSidebarWidth = useSelector(selectSidebarWidth);
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
const { activeWorkspaceMeta } = workspaceData || {};
const requestPaneRef = useRef<HTMLElement>(null);
const responsePaneRef = useRef<HTMLElement>(null);
const sidebarRef = useRef<HTMLElement>(null);
@@ -215,9 +212,9 @@ export const SidebarLayout: FC<Props> = ({
const [draggingSidebar, setDraggingSidebar] = useState(false);
const [draggingPaneHorizontal, setDraggingPaneHorizontal] = useState(false);
const [draggingPaneVertical, setDraggingPaneVertical] = useState(false);
const [sidebarWidth, setSidebarWidth] = useState(reduxSidebarWidth || DEFAULT_SIDEBAR_WIDTH);
const [paneWidth, setPaneWidth] = useState(reduxPaneWidth || DEFAULT_PANE_WIDTH);
const [paneHeight, setPaneHeight] = useState(reduxPaneHeight || DEFAULT_PANE_HEIGHT);
const [sidebarWidth, setSidebarWidth] = useState(activeWorkspaceMeta?.sidebarWidth || DEFAULT_SIDEBAR_WIDTH);
const [paneWidth, setPaneWidth] = useState(activeWorkspaceMeta?.paneWidth || DEFAULT_PANE_WIDTH);
const [paneHeight, setPaneHeight] = useState(activeWorkspaceMeta?.paneHeight || DEFAULT_PANE_HEIGHT);
useEffect(() => {
const unsubscribe = window.main.on('toggle-sidebar', () => {
@@ -265,7 +262,7 @@ export const SidebarLayout: FC<Props> = ({
const handleMouseMove = useCallback((event: MouseEvent) => {
if (draggingPaneHorizontal) {
// Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
const distance = reduxPaneWidth - paneWidth;
const distance = (activeWorkspaceMeta?.paneWidth || DEFAULT_PANE_WIDTH) - paneWidth;
if (!showDragOverlay && Math.abs(distance) > 0.02) {
setShowDragOverlay(true);
@@ -283,7 +280,7 @@ export const SidebarLayout: FC<Props> = ({
}
} else if (draggingPaneVertical) {
// Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
const distance = reduxPaneHeight - paneHeight;
const distance = (activeWorkspaceMeta?.paneHeight || DEFAULT_PANE_HEIGHT) - paneHeight;
/* % */
if (!showDragOverlay && Math.abs(distance) > 0.02) {
setShowDragOverlay(true);
@@ -300,7 +297,7 @@ export const SidebarLayout: FC<Props> = ({
}
} else if (draggingSidebar) {
// Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
const distance = reduxSidebarWidth - sidebarWidth;
const distance = (activeWorkspaceMeta?.sidebarWidth || DEFAULT_SIDEBAR_WIDTH) - sidebarWidth;
/* ems */
if (!showDragOverlay && Math.abs(distance) > 2) {
setShowDragOverlay(true);
@@ -317,7 +314,7 @@ export const SidebarLayout: FC<Props> = ({
handleSetSidebarWidth(localSidebarWidth);
}
}
}, [draggingPaneHorizontal, draggingPaneVertical, draggingSidebar, handleSetPaneHeight, handleSetPaneWidth, handleSetSidebarWidth, paneHeight, paneWidth, reduxPaneHeight, reduxPaneWidth, reduxSidebarWidth, showDragOverlay, sidebarWidth]);
}, [activeWorkspaceMeta?.paneHeight, activeWorkspaceMeta?.paneWidth, activeWorkspaceMeta?.sidebarWidth, draggingPaneHorizontal, draggingPaneVertical, draggingSidebar, handleSetPaneHeight, handleSetPaneWidth, handleSetSidebarWidth, paneHeight, paneWidth, showDragOverlay, sidebarWidth]);
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);

View File

@@ -1,32 +1,34 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { createRequest, CreateRequestType } from '../../hooks/create-request';
import { createRequestGroup } from '../../hooks/create-request-group';
import { selectActiveWorkspace, selectHotKeyRegistry } from '../../redux/selectors';
import { selectHotKeyRegistry } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown';
export const SidebarCreateDropdown = () => {
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeWorkspaceId = activeWorkspace?._id;
const {
activeWorkspace,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const create = useCallback((value: CreateRequestType) => {
if (activeWorkspaceId) {
if (activeWorkspace._id) {
createRequest({
requestType: value,
parentId: activeWorkspaceId,
workspaceId: activeWorkspaceId,
parentId: activeWorkspace._id,
workspaceId: activeWorkspace._id,
});
}
}, [activeWorkspaceId]);
}, [activeWorkspace._id]);
const createGroup = useCallback(() => {
if (!activeWorkspaceId) {
if (!activeWorkspace._id) {
return;
}
createRequestGroup(activeWorkspaceId);
}, [activeWorkspaceId]);
createRequestGroup(activeWorkspace._id);
}, [activeWorkspace._id]);
const dataTestId = 'SidebarCreateDropdown';
return (
<Dropdown

View File

@@ -1,29 +1,29 @@
import React, { FC, useCallback, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { SortOrder } from '../../../common/constants';
import { database as db } from '../../../common/database';
import { sortMethodMap } from '../../../common/sorting';
import * as models from '../../../models';
import { isRequestGroup } from '../../../models/request-group';
import { selectActiveWorkspace, selectActiveWorkspaceMeta } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { useDocBodyKeyboardShortcuts } from '../keydown-binder';
import { SidebarCreateDropdown } from './sidebar-create-dropdown';
import { SidebarSortDropdown } from './sidebar-sort-dropdown';
interface Props {
filter: string;
}
export const SidebarFilter: FC<Props> = ({ filter }) => {
const inputRef = useRef<HTMLInputElement>(null);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const {
activeWorkspace,
activeWorkspaceMeta,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const handleClearFilter = useCallback(async () => {
if (activeWorkspaceMeta) {
await models.workspaceMeta.update(activeWorkspaceMeta, { sidebarFilter: '' });
}
await models.workspaceMeta.update(activeWorkspaceMeta, { sidebarFilter: '' });
if (inputRef.current) {
inputRef.current.value = '';
inputRef.current.focus();
@@ -31,9 +31,7 @@ export const SidebarFilter: FC<Props> = ({ filter }) => {
}, [activeWorkspaceMeta]);
const handleOnChange = useCallback(async (event: React.SyntheticEvent<HTMLInputElement>) => {
if (activeWorkspaceMeta) {
await models.workspaceMeta.update(activeWorkspaceMeta, { sidebarFilter: event.currentTarget.value });
}
await models.workspaceMeta.update(activeWorkspaceMeta, { sidebarFilter: event.currentTarget.value });
}, [activeWorkspaceMeta]);
useDocBodyKeyboardShortcuts({
@@ -44,9 +42,6 @@ export const SidebarFilter: FC<Props> = ({ filter }) => {
const sortSidebar = async (order: SortOrder, parentId?: string) => {
let flushId: number | undefined;
if (!activeWorkspace) {
return;
}
if (!parentId) {
parentId = activeWorkspace._id;
flushId = await db.bufferChanges();

View File

@@ -1,7 +1,7 @@
import classnames from 'classnames';
import React, { FC, forwardRef, MouseEvent, ReactElement, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { DragSource, DragSourceSpec, DropTarget, DropTargetSpec } from 'react-dnd';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { CONTENT_TYPE_GRAPHQL } from '../../../common/constants';
import { getMethodOverrideHeader } from '../../../common/misc';
@@ -14,7 +14,7 @@ import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-
import { useNunjucks } from '../../context/nunjucks/use-nunjucks';
import { createRequest, updateRequestMetaByParentId } from '../../hooks/create-request';
import { useReadyState } from '../../hooks/use-ready-state';
import { selectActiveEnvironment, selectActiveProject, selectActiveWorkspace, selectActiveWorkspaceMeta } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import type { DropdownHandle } from '../base/dropdown';
import { Editable } from '../base/editable';
import { Highlight } from '../base/highlight';
@@ -27,7 +27,6 @@ import { MethodTag } from '../tags/method-tag';
import { WebSocketTag } from '../tags/websocket-tag';
import { ConnectionCircle } from '../websockets/action-bar';
import { DnDProps, DragObject, dropHandleCreator, hoverHandleCreator, sourceCollect, targetCollect } from './dnd';
interface RawProps {
disableDragAndDrop?: boolean;
filter: string;
@@ -68,11 +67,14 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
requestGroup,
}, ref) => {
const { handleRender } = useNunjucks();
const activeProject = useSelector(selectActiveProject);
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const activeWorkspaceId = activeWorkspace?._id;
const {
activeWorkspace,
activeWorkspaceMeta,
activeEnvironment,
activeProject,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const activeWorkspaceId = activeWorkspace._id;
const [dragDirection, setDragDirection] = useState(0);
const [isEditing, setIsEditing] = useState(false);
const handleSetActiveRequest = useCallback(() => {
@@ -236,7 +238,7 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
);
} else {
let methodTag = null;
let methodTag;
if (isGrpcRequest(request)) {
methodTag = <GrpcTag />;

View File

@@ -1,10 +1,9 @@
import React, { FC, ReactNode, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { VCS } from '../../sync/vcs/vcs';
import { selectActiveProject } from '../redux/selectors';
import { WorkspaceLoaderData } from '../routes/workspace';
import { showError } from './modals';
interface Props {
vcs: VCS;
branch: string;
@@ -16,7 +15,9 @@ interface Props {
export const SyncPullButton: FC<Props> = props => {
const { className, children, disabled } = props;
const project = useSelector(selectActiveProject);
const {
activeProject,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const [loading, setLoading] = useState(false);
const onClick = async () => {
const { vcs, onPull, branch } = props;
@@ -27,7 +28,7 @@ export const SyncPullButton: FC<Props> = props => {
try {
// Clone old VCS so we don't mess anything up while working on other projects
await newVCS.checkout([], branch);
await newVCS.pull([], project.remoteId);
await newVCS.pull([], activeProject.remoteId);
} catch (err) {
showError({
title: 'Pull Error',

View File

@@ -1,195 +0,0 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { renderHook } from '@testing-library/react';
import { mocked } from 'jest-mock';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import type { PromiseValue } from 'type-fest';
import { globalBeforeEach } from '../../../../__jest__/before-each';
import { reduxStateForTest } from '../../../../__jest__/redux-state-for-test';
import { withReduxStore } from '../../../../__jest__/with-redux-store';
import { ACTIVITY_DEBUG } from '../../../../common/constants';
import { getRenderContext, getRenderContextAncestors, render } from '../../../../common/render';
import * as models from '../../../../models';
import { RootState } from '../../../redux/modules';
import { initializeNunjucksRenderPromiseCache, useNunjucks } from '../use-nunjucks';
const renderMock = mocked(render);
const getRenderContextMock = mocked(getRenderContext);
const getRenderContextAncestorsMock = mocked(getRenderContextAncestors);
jest.mock('../../../../common/render', () => ({
render: jest.fn(),
getRenderContext: jest.fn(),
getRenderContextAncestors: jest.fn(),
}));
const middlewares = [thunk];
const mockStore = configureMockStore<RootState>(middlewares);
const mockAncestors: PromiseValue<ReturnType<typeof getRenderContextAncestorsMock>> = [];
const mockContext: PromiseValue<ReturnType<typeof getRenderContextMock>> = { foo: 'bar' };
describe('useNunjucks', () => {
beforeEach(async () => {
await globalBeforeEach();
getRenderContextMock.mockResolvedValue(mockContext);
getRenderContextAncestorsMock.mockResolvedValue(mockAncestors);
initializeNunjucksRenderPromiseCache();
});
describe('handleGetRenderContext', () => {
it('should return context with keys', async () => {
const store = mockStore(await reduxStateForTest({
activeActivity: ACTIVITY_DEBUG,
}));
const { result } = renderHook(useNunjucks, { wrapper: withReduxStore(store) });
const context = await result.current.handleGetRenderContext();
expect(context).toStrictEqual({
context: mockContext,
keys: [{
name: '_.foo',
value: 'bar',
}],
});
});
it('should get context using the active entities', async () => {
// Arrange
const workspace = await models.workspace.create();
await models.workspaceMeta.getOrCreateByParentId(workspace._id);
const environment = await models.environment.getOrCreateForParentId(workspace._id);
const request = await models.request.create({ parentId: workspace._id });
await models.workspaceMeta.updateByParentId(workspace._id, {
activeEnvironmentId: environment._id,
activeRequestId: request._id,
});
const store = mockStore(await reduxStateForTest({
activeActivity: ACTIVITY_DEBUG,
activeWorkspaceId: workspace._id,
}));
// Act
const { result } = renderHook(useNunjucks, { wrapper: withReduxStore(store) });
await result.current.handleGetRenderContext();
// Assert
expect(getRenderContextAncestorsMock).toBeCalledWith(request);
expect(getRenderContextMock).toBeCalledWith({
request,
environmentId: environment._id,
ancestors: mockAncestors,
});
});
});
describe('handleRender', () => {
it('should render and get context once', async () => {
// Arrange
const store = mockStore(await reduxStateForTest());
// Act
const { result } = renderHook(useNunjucks, { wrapper: withReduxStore(store) });
await result.current.handleRender('abc');
// Assert
expect(getRenderContextMock).toHaveBeenCalledTimes(1);
expect(renderMock).toHaveBeenCalledTimes(1);
});
it('should render and get context twice because there is no caching', async () => {
// Arrange
const store = mockStore(await reduxStateForTest());
// Act
const { result } = renderHook(useNunjucks, { wrapper: withReduxStore(store) });
await result.current.handleRender('abc');
await result.current.handleRender('def');
// Assert
expect(getRenderContextMock).toHaveBeenCalledTimes(2);
expect(renderMock).toHaveBeenCalledTimes(2);
});
it('should render and get context once because there is a cache', async () => {
// Arrange
const store = mockStore(await reduxStateForTest());
// Act
const { result } = renderHook(useNunjucks, { wrapper: withReduxStore(store) });
const cacheKey = 'cache';
await result.current.handleRender('abc', cacheKey);
await result.current.handleRender('def', cacheKey);
// Assert
expect(getRenderContextMock).toHaveBeenCalledTimes(1);
expect(renderMock).toHaveBeenCalledTimes(2);
});
it('should render and get context twice because there are different cache keys', async () => {
// Arrange
const store = mockStore(await reduxStateForTest());
// Act
const { result } = renderHook(useNunjucks, { wrapper: withReduxStore(store) });
const cacheKeyOne = 'cache-1';
const cacheKeyTwo = 'cache-2';
await result.current.handleRender('abc', cacheKeyOne);
await result.current.handleRender('def', cacheKeyTwo);
await result.current.handleRender('ghi', cacheKeyOne);
await result.current.handleRender('jkl', cacheKeyTwo);
// Assert
expect(getRenderContextMock).toHaveBeenCalledTimes(2);
expect(renderMock).toHaveBeenCalledTimes(4);
});
it('should not change the cache during re-renders of the hook', async () => {
// Arrange
const store = mockStore(await reduxStateForTest());
// Act
const { result, rerender } = renderHook(useNunjucks, { wrapper: withReduxStore(store) });
const cacheKeyOne = 'cache-1';
const cacheKeyTwo = 'cache-2';
await result.current.handleRender('abc', cacheKeyOne);
rerender();
await result.current.handleRender('def', cacheKeyTwo);
rerender();
await result.current.handleRender('ghi', cacheKeyOne);
rerender();
await result.current.handleRender('jkl', cacheKeyTwo);
// Assert
expect(getRenderContextMock).toHaveBeenCalledTimes(2);
expect(renderMock).toHaveBeenCalledTimes(4);
});
it('should not change the cache during multiple renders of the hook', async () => {
// Arrange
const store = mockStore(await reduxStateForTest());
// Act
const cacheKeyOne = 'cache-1';
const cacheKeyTwo = 'cache-2';
await renderHook(useNunjucks, { wrapper: withReduxStore(store) }).result.current.handleRender('abc', cacheKeyOne);
await renderHook(useNunjucks, { wrapper: withReduxStore(store) }).result.current.handleRender('def', cacheKeyTwo);
await renderHook(useNunjucks, { wrapper: withReduxStore(store) }).result.current.handleRender('ghi', cacheKeyOne);
await renderHook(useNunjucks, { wrapper: withReduxStore(store) }).result.current.handleRender('jkl', cacheKeyTwo);
// Assert
expect(getRenderContextMock).toHaveBeenCalledTimes(2);
expect(renderMock).toHaveBeenCalledTimes(4);
});
});
});

View File

@@ -1,11 +1,12 @@
import { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { getRenderContext, getRenderContextAncestors, HandleGetRenderContext, HandleRender, render } from '../../../common/render';
import { NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME } from '../../../templating';
import { getKeys } from '../../../templating/utils';
import { selectActiveEnvironment, selectActiveRequest, selectActiveWorkspace } from '../../redux/selectors';
import { selectActiveRequest } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
let getRenderContextPromiseCache: any = {};
export const initializeNunjucksRenderPromiseCache = () => {
@@ -18,18 +19,19 @@ initializeNunjucksRenderPromiseCache();
* Access to functions useful for Nunjucks rendering
*/
export const useNunjucks = () => {
const environmentId = useSelector(selectActiveEnvironment)?._id;
const request = useSelector(selectActiveRequest);
const workspace = useSelector(selectActiveWorkspace);
const {
activeWorkspace,
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const fetchRenderContext = useCallback(async () => {
const ancestors = await getRenderContextAncestors(request || workspace);
const ancestors = await getRenderContextAncestors(request || activeWorkspace);
return getRenderContext({
request: request || undefined,
environmentId,
environmentId: activeEnvironment._id,
ancestors,
});
}, [environmentId, request, workspace]);
}, [activeEnvironment._id, request, activeWorkspace]);
const handleGetRenderContext: HandleGetRenderContext = useCallback(async () => {
const context = await fetchRenderContext();

View File

@@ -1,25 +1,25 @@
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { ACTIVITY_HOME, getProductName } from '../../common/constants';
import { selectActiveActivity, selectActiveEnvironment, selectActiveProject, selectActiveRequest, selectActiveWorkspace, selectActiveWorkspaceName } from '../redux/selectors';
import { getProductName } from '../../common/constants';
import { selectActiveRequest } from '../redux/selectors';
import { WorkspaceLoaderData } from '../routes/workspace';
export const useDocumentTitle = () => {
const activeActivity = useSelector(selectActiveActivity);
const activeProject = useSelector(selectActiveProject);
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
const activeWorkspace = useSelector(selectActiveWorkspace);
const {
activeWorkspace,
activeEnvironment,
activeProject,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeRequest = useSelector(selectActiveRequest);
// Update document title
useEffect(() => {
let title;
if (activeActivity === ACTIVITY_HOME) {
title = getProductName();
} else if (activeWorkspace && activeWorkspaceName) {
if (activeWorkspace && activeWorkspace.name) {
title = activeProject.name;
title += ` - ${activeWorkspaceName}`;
title += ` - ${activeWorkspace.name}`;
if (activeEnvironment) {
title += ` (${activeEnvironment.name})`;
}
@@ -28,6 +28,6 @@ export const useDocumentTitle = () => {
}
}
document.title = title || getProductName();
}, [activeActivity, activeEnvironment, activeProject.name, activeRequest, activeWorkspace, activeWorkspaceName]);
}, [activeEnvironment, activeProject.name, activeRequest, activeWorkspace]);
};

View File

@@ -1,4 +1,5 @@
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import * as models from '../../models';
import * as plugins from '../../plugins';
@@ -6,13 +7,12 @@ import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder';
import { showModal } from '../components/modals';
import { SettingsModal, TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal';
import { WorkspaceSettingsModal } from '../components/modals/workspace-settings-modal';
import { selectActiveWorkspace, selectActiveWorkspaceMeta, selectSettings } from '../redux/selectors';
import { selectSettings } from '../redux/selectors';
import { WorkspaceLoaderData } from '../routes/workspace';
export const useGlobalKeyboardShortcuts = () => {
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
const settings = useSelector(selectSettings);
const { activeWorkspace, activeWorkspaceMeta } = workspaceData || {};
useDocBodyKeyboardShortcuts({
workspace_showSettings:
() => activeWorkspace && showModal(WorkspaceSettingsModal),

View File

@@ -1,14 +1,13 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { ChangeBufferEvent, database } from '../../common/database';
import { BaseModel } from '../../models';
import {
selectActiveApiSpec,
selectActiveRequest,
selectActiveWorkspaceMeta,
} from '../redux/selectors';
import { WorkspaceLoaderData } from '../routes/workspace';
// We use this hook to determine if the active request has been updated from the system (not the user typing)
// For example, by pulling a new version from the remote, switching branches, etc.
export function useActiveRequestSyncVCSVersion() {
@@ -27,8 +26,9 @@ export function useActiveRequestSyncVCSVersion() {
// For example, by pulling a new version from the remote, switching branches, etc.
export function useActiveApiSpecSyncVCSVersion() {
const [version, setVersion] = useState(0);
const activeApiSpec = useSelector(selectActiveApiSpec);
const {
activeApiSpec,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
useEffect(() => {
const isRequestUpdatedFromSync = (changes: ChangeBufferEvent<BaseModel>[]) => changes.find(([, doc, fromSync]) => activeApiSpec?._id === doc._id && fromSync);
database.onChange(changes => isRequestUpdatedFromSync(changes) && setVersion(v => v + 1));
@@ -40,7 +40,8 @@ export function useActiveApiSpecSyncVCSVersion() {
// We use this hook to determine if the active workspace has been updated from the Git VCS
// For example, by pulling a new version from the remote, switching branches, etc.
export function useGitVCSVersion() {
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const {
activeWorkspaceMeta,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
return ((activeWorkspaceMeta?.cachedGitLastCommitTime + '') + activeWorkspaceMeta?.cachedGitRepositoryBranch) + '';
}

View File

@@ -22,19 +22,17 @@ import { database } from '../common/database';
import { initializeLogging } from '../common/log';
import * as models from '../models';
import { DEFAULT_ORGANIZATION_ID } from '../models/organization';
import { DEFAULT_PROJECT_ID, isRemoteProject } from '../models/project';
import { DEFAULT_PROJECT_ID } from '../models/project';
import { initNewOAuthSession } from '../network/o-auth-2/get-token';
import { init as initPlugins } from '../plugins';
import { applyColorScheme } from '../plugins/misc';
import { invariant } from '../utils/invariant';
import { AppLoadingIndicator } from './components/app-loading-indicator';
import { init as initStore, RootState } from './redux/modules';
import { init as initStore } from './redux/modules';
import {
setActiveActivity,
setActiveProject,
setActiveWorkspace,
} from './redux/modules/global';
import { selectActiveProject } from './redux/selectors';
import { ErrorRoute } from './routes/error';
import Root from './routes/root';
import { initializeSentry } from './sentry';
@@ -500,7 +498,6 @@ function updateReduxNavigationState(store: Store, pathname: string) {
store.dispatch(
setActiveWorkspace(isActivityDebug?.params.workspaceId || '')
);
store.dispatch(setActiveActivity(ACTIVITY_DEBUG));
} else if (isActivityDesign) {
currentActivity = ACTIVITY_SPEC;
store.dispatch(
@@ -509,7 +506,6 @@ function updateReduxNavigationState(store: Store, pathname: string) {
store.dispatch(
setActiveWorkspace(isActivityDesign?.params.workspaceId || '')
);
store.dispatch(setActiveActivity(ACTIVITY_SPEC));
} else if (isActivityTest) {
currentActivity = ACTIVITY_UNIT_TEST;
store.dispatch(
@@ -518,13 +514,11 @@ function updateReduxNavigationState(store: Store, pathname: string) {
store.dispatch(
setActiveWorkspace(isActivityTest?.params.workspaceId || '')
);
store.dispatch(setActiveActivity(ACTIVITY_UNIT_TEST));
} else {
currentActivity = ACTIVITY_HOME;
store.dispatch(
setActiveProject(isActivityHome?.params.projectId || '')
);
store.dispatch(setActiveActivity(ACTIVITY_HOME));
}
return currentActivity;
@@ -549,46 +543,14 @@ async function renderApp() {
// Synchronizes the Redux store with the router history
// @HACK: This is temporary until we completely remove navigation through Redux
const synchronizeRouterState = () => {
let currentActivity = (store.getState() as RootState).global.activeActivity;
let currentPathname = router.state.location.pathname;
currentActivity = updateReduxNavigationState(store, router.state.location.pathname);
updateReduxNavigationState(store, router.state.location.pathname);
router.subscribe(({ location }) => {
if (location.pathname !== currentPathname) {
currentPathname = location.pathname;
currentActivity = updateReduxNavigationState(store, location.pathname);
updateReduxNavigationState(store, location.pathname);
}
});
store.subscribe(() => {
const state = store.getState() as RootState;
const activity = state.global.activeActivity;
const activeProject = selectActiveProject(state);
const organizationId = activeProject && isRemoteProject(activeProject) ? activeProject._id : DEFAULT_ORGANIZATION_ID;
if (activity !== currentActivity) {
currentActivity = activity;
const activeProjectId = activeProject ? activeProject._id : DEFAULT_PROJECT_ID;
if (activity === ACTIVITY_HOME) {
router.navigate(`/organization/${organizationId}/project/${activeProject._id}`);
} else if (activity === ACTIVITY_DEBUG) {
router.navigate(
`/organization/${organizationId}/project/${activeProjectId}/workspace/${state.global.activeWorkspaceId}/${ACTIVITY_DEBUG}`
);
} else if (activity === ACTIVITY_SPEC) {
router.navigate(
`/organization/${organizationId}/project/${activeProjectId}/workspace/${state.global.activeWorkspaceId}/${ACTIVITY_SPEC}`
);
} else if (activity === ACTIVITY_UNIT_TEST) {
router.navigate(
`/organization/${organizationId}/project/${state.global.activeProjectId}/workspace/${state.global.activeWorkspaceId}/test`
);
}
}
});
};
synchronizeRouterState();

View File

@@ -1,190 +0,0 @@
import { beforeEach, describe, expect, it } from '@jest/globals';
import { globalBeforeEach } from '../../../__jest__/before-each';
import { reduxStateForTest } from '../../../__jest__/redux-state-for-test';
import { ACTIVITY_DEBUG, ACTIVITY_HOME } from '../../../common/constants';
import * as models from '../../../models';
import { DEFAULT_PROJECT_ID, Project } from '../../../models/project';
import { WorkspaceScopeKeys } from '../../../models/workspace';
import { selectActiveApiSpec, selectActiveProject, selectActiveWorkspaceName, selectWorkspacesWithResolvedNameForActiveProject } from '../selectors';
describe('selectors', () => {
beforeEach(globalBeforeEach);
describe('selectActiveProject', () => {
it('should return the active project', async () => {
// create two projects
const projectA = await models.project.create();
await models.project.create();
// set first as selected
const state = await reduxStateForTest({ activeProjectId: projectA._id });
const project = selectActiveProject(state);
expect(project).toStrictEqual(projectA);
});
it('should return default project if active project not found', async () => {
// create two projects
await models.project.create();
await models.project.create();
// set first as selected
const state = await reduxStateForTest({ activeProjectId: 'some-other-project' });
const project = selectActiveProject(state);
expect(project).toStrictEqual(expect.objectContaining<Partial<Project>>({ _id: DEFAULT_PROJECT_ID }));
});
it('should return default project if no active project', async () => {
// create two projects
await models.project.create();
await models.project.create();
// set nothing as active
const state = await reduxStateForTest({ activeProjectId: undefined });
const project = selectActiveProject(state);
expect(project).toStrictEqual(expect.objectContaining<Partial<Project>>({ _id: DEFAULT_PROJECT_ID }));
});
});
describe('selectActiveApiSpec', () => {
it('will return undefined when there is not an active workspace', async () => {
const state = await reduxStateForTest({
activeWorkspaceId: null,
});
expect(selectActiveApiSpec(state)).toBe(undefined);
});
it('will return the apiSpec for a given workspace', async () => {
const workspace = await models.workspace.create({
name: 'workspace.name',
scope: WorkspaceScopeKeys.design,
});
const spec = await models.apiSpec.updateOrCreateForParentId(
workspace._id,
{ fileName: 'apiSpec.fileName' },
);
const state = await reduxStateForTest({
activeActivity: ACTIVITY_DEBUG,
activeWorkspaceId: workspace._id,
});
expect(selectActiveApiSpec(state)).toEqual(spec);
});
});
describe('selectActiveWorkspaceName', () => {
it('returns workspace name for collections', async () => {
const workspace = await models.workspace.create({
name: 'workspace.name',
scope: WorkspaceScopeKeys.collection,
});
// even though this shouldn't technically happen, we want to make sure the selector still makes the right decision (and ignores the api spec for collections)
await models.apiSpec.updateOrCreateForParentId(
workspace._id,
{ fileName: 'apiSpec.fileName' },
);
const state = await reduxStateForTest({
activeActivity: ACTIVITY_DEBUG,
activeWorkspaceId: workspace._id,
});
expect(selectActiveWorkspaceName(state)).toBe('workspace.name');
});
it('returns api spec name for design documents', async () => {
const workspace = await models.workspace.create({
name: 'workspace.name',
scope: WorkspaceScopeKeys.design,
});
await models.apiSpec.updateOrCreateForParentId(
workspace._id,
{ fileName: 'apiSpec.fileName' },
);
const state = await reduxStateForTest({
activeActivity: ACTIVITY_DEBUG,
activeWorkspaceId: workspace._id,
});
expect(selectActiveWorkspaceName(state)).toBe('apiSpec.fileName');
});
it('returns undefined when there is not an active workspace', async () => {
await models.workspace.create({
name: 'workspace.name',
scope: WorkspaceScopeKeys.collection,
});
const state = await reduxStateForTest({
activeActivity: ACTIVITY_DEBUG,
activeWorkspaceId: null,
});
expect(selectActiveWorkspaceName(state)).toBe(undefined);
});
});
describe('selectWorkspacesWithResolvedNameForActiveProject', () => {
it('returns the workspaces with resolved names for the active project', async () => {
const newCollectionWorkspace = await models.workspace.create({
name: 'collectionWorkspace.name',
scope: WorkspaceScopeKeys.collection,
});
const newDesignWorkspace = await models.workspace.create({
name: 'designWorkspace.name',
scope: WorkspaceScopeKeys.design,
});
const newApiSpec = await models.apiSpec.getOrCreateForParentId(
newDesignWorkspace._id
);
// The database will update the api spec with the workspace name
// That's why we need to explicitly update the ApiSpec name
await models.apiSpec.update(newApiSpec, {
fileName: 'apiSpec.name',
});
const state = await reduxStateForTest({
activeActivity: ACTIVITY_HOME,
activeWorkspaceId: null,
});
const workspaces = selectWorkspacesWithResolvedNameForActiveProject(state);
const designWorkspace = workspaces.find(
workspace => workspace._id === newDesignWorkspace._id
);
const collectionWorkspace = workspaces.find(
workspace => workspace._id === newCollectionWorkspace._id
);
expect(
designWorkspace
).toMatchObject(
{
_id: newDesignWorkspace._id,
name: 'apiSpec.name',
scope: WorkspaceScopeKeys.design,
type: 'Workspace',
},
);
expect(
collectionWorkspace
).toMatchObject(
{
_id: newCollectionWorkspace._id,
name: 'collectionWorkspace.name',
scope: WorkspaceScopeKeys.collection,
type: 'Workspace',
},
);
});
});
});

View File

@@ -1,19 +1,10 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { globalBeforeEach } from '../../../../__jest__/before-each';
import {
ACTIVITY_DEBUG,
ACTIVITY_HOME,
ACTIVITY_SPEC,
ACTIVITY_UNIT_TEST,
GlobalActivity,
} from '../../../../common/constants';
import {
LOCALSTORAGE_PREFIX,
SET_ACTIVE_ACTIVITY,
SET_ACTIVE_PROJECT,
SET_ACTIVE_WORKSPACE,
setActiveActivity,
setActiveProject,
setActiveWorkspace,
} from '../global';
@@ -27,24 +18,6 @@ describe('global', () => {
global.localStorage.clear();
});
describe('setActiveActivity', () => {
it.each([
ACTIVITY_SPEC,
ACTIVITY_DEBUG,
ACTIVITY_UNIT_TEST,
ACTIVITY_HOME,
])('should update local storage and track event: %s', (activity: GlobalActivity) => {
const expectedEvent = {
type: SET_ACTIVE_ACTIVITY,
activity,
};
expect(setActiveActivity(activity)).toStrictEqual(expectedEvent);
expect(global.localStorage.getItem(`${LOCALSTORAGE_PREFIX}::activity`)).toBe(
JSON.stringify(activity),
);
});
});
describe('setActiveProject', () => {
it('should update local storage', () => {
const projectId = 'id';

View File

@@ -1,140 +0,0 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { globalBeforeEach } from '../../../../__jest__/before-each';
import { reduxStateForTest } from '../../../../__jest__/redux-state-for-test';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../../../../common/constants';
import * as models from '../../../../models';
import { SET_ACTIVE_ACTIVITY, SET_ACTIVE_PROJECT, SET_ACTIVE_WORKSPACE } from '../global';
import { activateWorkspace } from '../workspace';
jest.mock('../../../components/modals');
jest.mock('../../../../ui/analytics');
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('workspace', () => {
beforeEach(globalBeforeEach);
describe('activateWorkspace', () => {
it('should do nothing if workspace cannot be found', async () => {
const store = mockStore(await reduxStateForTest({ activeProjectId: 'abc', activeWorkspaceId: 'def' }));
await store.dispatch(activateWorkspace({ workspaceId: 'DOES_NOT_EXIST' }));
expect(store.getActions()).toEqual([]);
});
it('should activate project and workspace and activity using workspaceId', async () => {
const project = await models.project.create();
const workspace = await models.workspace.create({ scope: 'design', parentId: project._id });
const store = mockStore(await reduxStateForTest({ activeProjectId: 'abc', activeWorkspaceId: 'def' }));
await store.dispatch(activateWorkspace({ workspaceId: workspace._id }));
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,
projectId: project._id,
},
{
type: SET_ACTIVE_WORKSPACE,
workspaceId: workspace._id,
},
{
type: SET_ACTIVE_ACTIVITY,
activity: ACTIVITY_SPEC,
},
]);
});
it('should activate project and workspace and activity from home', async () => {
const project = await models.project.create();
const workspace = await models.workspace.create({ scope: 'design', parentId: project._id });
const store = mockStore(await reduxStateForTest({ activeProjectId: 'abc', activeWorkspaceId: 'def' }));
await store.dispatch(activateWorkspace({ workspace }));
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,
projectId: project._id,
},
{
type: SET_ACTIVE_WORKSPACE,
workspaceId: workspace._id,
},
{
type: SET_ACTIVE_ACTIVITY,
activity: ACTIVITY_SPEC,
},
]);
});
it('should switch to the default design activity', async () => {
const project = await models.project.create();
const workspace = await models.workspace.create({ scope: 'design', parentId: project._id });
const store = mockStore(await reduxStateForTest({ activeProjectId: project._id, activeWorkspaceId: workspace._id }));
await store.dispatch(activateWorkspace({ workspace }));
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,
projectId: project._id,
},
{
type: SET_ACTIVE_WORKSPACE,
workspaceId: workspace._id,
},
{
type: SET_ACTIVE_ACTIVITY,
activity: ACTIVITY_SPEC,
},
]);
});
it.each([ACTIVITY_DEBUG])('should not switch activity if already in a supported collection activity: %s', async activeActivity => {
const project = await models.project.create();
const workspace = await models.workspace.create({ scope: 'design', parentId: project._id });
const store = mockStore(await reduxStateForTest({ activeProjectId: project._id, activeWorkspaceId: workspace._id, activeActivity }));
await store.dispatch(activateWorkspace({ workspace }));
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,
projectId: project._id,
},
{
type: SET_ACTIVE_WORKSPACE,
workspaceId: workspace._id,
},
]);
});
it('should switch to the default collection activity', async () => {
const project = await models.project.create();
const workspace = await models.workspace.create({ scope: 'collection', parentId: project._id });
const store = mockStore(await reduxStateForTest({ activeProjectId: project._id, activeWorkspaceId: workspace._id }));
await store.dispatch(activateWorkspace({ workspace }));
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,
projectId: project._id,
},
{
type: SET_ACTIVE_WORKSPACE,
workspaceId: workspace._id,
},
{
type: SET_ACTIVE_ACTIVITY,
activity: ACTIVITY_DEBUG,
},
]);
});
});
});

View File

@@ -1,10 +1,6 @@
import { combineReducers } from 'redux';
import type { DashboardSortOrder, GlobalActivity } from '../../../common/constants';
import {
ACTIVITY_HOME,
isValidActivity,
} from '../../../common/constants';
import type { DashboardSortOrder } from '../../../common/constants';
import { DEFAULT_PROJECT_ID } from '../../../models/project';
export const LOCALSTORAGE_PREFIX = 'insomnia::meta';
@@ -17,15 +13,6 @@ export const SET_ACTIVE_ACTIVITY = 'global/activate-activity';
// ~~~~~~~~ //
// REDUCERS //
// ~~~~~~~~ //
function activeActivityReducer(state: string | null = null, action: any) {
switch (action.type) {
case SET_ACTIVE_ACTIVITY:
return action.activity;
default:
return state;
}
}
function activeProjectReducer(state: string = DEFAULT_PROJECT_ID, action: any) {
switch (action.type) {
@@ -71,7 +58,6 @@ export interface GlobalState {
activeProjectId: string;
dashboardSortOrder: DashboardSortOrder;
activeWorkspaceId: string | null;
activeActivity: GlobalActivity | null;
isLoggedIn: boolean;
}
@@ -79,7 +65,6 @@ export const reducer = combineReducers<GlobalState>({
dashboardSortOrder: dashboardSortOrderReducer,
activeProjectId: activeProjectReducer,
activeWorkspaceId: activeWorkspaceReducer,
activeActivity: activeActivityReducer,
isLoggedIn: loginStateChangeReducer,
});
@@ -91,19 +76,6 @@ export const loginStateChange = (loggedIn: boolean) => ({
loggedIn,
});
/*
Go to an explicit activity
*/
export const setActiveActivity = (activity: GlobalActivity) => {
activity = isValidActivity(activity) ? activity : ACTIVITY_HOME;
window.localStorage.setItem(`${LOCALSTORAGE_PREFIX}::activity`, JSON.stringify(activity));
window.main.trackPageView({ name: activity });
return {
type: SET_ACTIVE_ACTIVITY,
activity,
};
};
export const setActiveProject = (projectId: string) => {
const key = `${LOCALSTORAGE_PREFIX}::activeProjectId`;
window.localStorage.setItem(key, JSON.stringify(projectId));
@@ -113,17 +85,6 @@ export const setActiveProject = (projectId: string) => {
};
};
export const setDashboardSortOrder = (sortOrder: DashboardSortOrder) => {
const key = `${LOCALSTORAGE_PREFIX}::dashboard-sort-order`;
window.localStorage.setItem(key, JSON.stringify(sortOrder));
return {
type: SET_DASHBOARD_SORT_ORDER,
payload: {
sortOrder,
},
};
};
export const setActiveWorkspace = (workspaceId: string | null) => {
const key = `${LOCALSTORAGE_PREFIX}::activeWorkspaceId`;
window.localStorage.setItem(key, JSON.stringify(workspaceId));

View File

@@ -1,50 +0,0 @@
import { Dispatch } from 'redux';
import type { RequireExactlyOne } from 'type-fest';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, GlobalActivity, isCollectionActivity, isDesignActivity } from '../../../common/constants';
import * as models from '../../../models';
import { isCollection, isDesign, Workspace } from '../../../models/workspace';
import { selectActiveActivity, selectWorkspaces } from '../selectors';
import { RootState } from '.';
import { setActiveActivity, setActiveProject, setActiveWorkspace } from './global';
export const activateWorkspace = ({ workspace, workspaceId }: RequireExactlyOne<{workspace: Workspace; workspaceId: string}>) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
// If we have no workspace but we do have an id, search for it
if (!workspace && workspaceId) {
workspace = selectWorkspaces(getState()).find(({ _id }) => _id === workspaceId);
}
// If we still have no workspace, exit
if (!workspace) {
return;
}
const activeActivity = selectActiveActivity(getState()) || undefined;
// Activate the correct project
const nextProjectId = workspace.parentId;
dispatch(setActiveProject(nextProjectId));
// Activate the correct workspace
const nextWorkspaceId = workspace._id;
dispatch(setActiveWorkspace(nextWorkspaceId));
// Activate the correct activity
if (isCollection(workspace) && isCollectionActivity(activeActivity)) {
// we are in a collection, and our active activity is a collection activity
return;
}
if (isDesign(workspace) && isDesignActivity(activeActivity)) {
// we are in a design document, and our active activity is a design activity
return;
}
const { activeActivity: cachedActivity } = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
const nextActivity = cachedActivity as GlobalActivity || (isDesign(workspace) ? ACTIVITY_SPEC : ACTIVITY_DEBUG);
dispatch(setActiveActivity(nextActivity));
// TODO: dispatch one action to activate the project, workspace and activity in one go to avoid jumps in the UI
};
};

View File

@@ -1,7 +1,7 @@
import { createSelector } from 'reselect';
import type { ValueOf } from 'type-fest';
import { isWorkspaceActivity, PREVIEW_MODE_SOURCE } from '../../common/constants';
import { PREVIEW_MODE_SOURCE } from '../../common/constants';
import * as models from '../../models';
import { BaseModel } from '../../models';
import { GrpcRequest, isGrpcRequest } from '../../models/grpc-request';
@@ -11,10 +11,8 @@ 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 { UnitTestResult } from '../../models/unit-test-result';
import { isWebSocketRequest, WebSocketRequest } from '../../models/websocket-request';
import { type WebSocketResponse } from '../../models/websocket-response';
import { isCollection } from '../../models/workspace';
import { RootState } from './modules';
type EntitiesLists = {
@@ -24,43 +22,29 @@ type EntitiesLists = {
// ~~~~~~~~~ //
// Selectors //
// ~~~~~~~~~ //
export const selectEntities = createSelector(
(state: RootState) => state.entities,
entities => entities,
);
export const selectGlobal = createSelector(
(state: RootState) => state.global,
global => global,
);
export const selectEntitiesLists = createSelector(
selectEntities,
(state: RootState) => state.entities,
entities => {
// transforms entities object from object keyed on id to array of entities containing id
const entitiesLists: any = {};
for (const k of Object.keys(entities)) {
const entityMap = (entities as any)[k];
entitiesLists[k] = Object.keys(entityMap).map(id => entityMap[id]);
for (const [k, v] of Object.entries(entities)) {
entitiesLists[k] = Object.keys(v).map(id => v[id]);
}
return entitiesLists as EntitiesLists;
},
);
export const selectEntitiesChildrenMap = createSelector(selectEntitiesLists, entities => {
const parentLookupMap: any = {};
for (const key of Object.keys(entities)) {
for (const entity of (entities as any)[key]) {
if (!entity.parentId) {
continue;
}
if (parentLookupMap[entity.parentId]) {
parentLookupMap[entity.parentId].push(entity);
} else {
parentLookupMap[entity.parentId] = [entity];
// group entities by parent
for (const value of Object.values(entities)) {
for (const entity of value) {
if (entity.parentId) {
if (parentLookupMap[entity.parentId]) {
parentLookupMap[entity.parentId].push(entity);
} else {
parentLookupMap[entity.parentId] = [entity];
}
}
}
}
@@ -96,50 +80,21 @@ export const selectRemoteProjects = createSelector(
projects => projects.filter(isRemoteProject),
);
export const selectActiveProject = createSelector(
selectEntities,
(state: RootState) => state.global.activeProjectId,
(entities, activeProjectId) => {
return entities.projects[activeProjectId] || entities.projects[DEFAULT_PROJECT_ID];
},
);
export const selectDashboardSortOrder = createSelector(
selectGlobal,
global => global.dashboardSortOrder
);
export const selectWorkspaces = createSelector(
selectEntitiesLists,
entities => entities.workspaces,
);
export const selectWorkspacesForActiveProject = createSelector(
selectWorkspaces,
selectActiveProject,
(workspaces, activeProject) => workspaces.filter(workspace => workspace.parentId === activeProject._id),
selectEntitiesLists,
(state: RootState) => state.global.activeProjectId,
(entities, activeProjectId) => entities.workspaces.filter(workspace => workspace.parentId === (activeProjectId || DEFAULT_PROJECT_ID)),
);
export const selectActiveWorkspace = createSelector(
selectWorkspacesForActiveProject,
(state: RootState) => state.global.activeWorkspaceId,
(state: RootState) => state.global.activeActivity,
(workspaces, activeWorkspaceId, activeActivity) => {
// Only return an active workspace if we're in an activity
if (activeActivity && isWorkspaceActivity(activeActivity)) {
const workspace = workspaces.find(workspace => workspace._id === activeWorkspaceId);
return workspace;
}
return undefined;
(workspaces, activeWorkspaceId) => {
const workspace = workspaces.find(workspace => workspace._id === activeWorkspaceId);
return workspace;
},
);
export const selectWorkspaceMetas = createSelector(
selectEntitiesLists,
entities => entities.workspaceMetas,
);
export const selectActiveWorkspaceMeta = createSelector(
selectActiveWorkspace,
selectEntitiesLists,
@@ -154,57 +109,6 @@ export const selectApiSpecs = createSelector(
entities => entities.apiSpecs,
);
export const selectWorkspacesWithResolvedNameForActiveProject = createSelector(
selectWorkspacesForActiveProject,
selectApiSpecs,
(workspaces, apiSpecs) => {
return workspaces.map(workspace => {
if (isCollection(workspace)) {
return workspace;
}
const apiSpec = apiSpecs.find(
apiSpec => apiSpec.parentId === workspace._id
);
return {
...workspace,
name: apiSpec?.fileName || workspace.name,
};
});
}
);
export const selectActiveApiSpec = createSelector(
selectApiSpecs,
selectActiveWorkspace,
(apiSpecs, activeWorkspace) => {
if (!activeWorkspace) {
// There should never be an active api spec without an active workspace
return undefined;
}
return apiSpecs.find(apiSpec => apiSpec.parentId === activeWorkspace._id);
}
);
export const selectActiveWorkspaceName = createSelector(
selectActiveWorkspace,
selectActiveApiSpec,
(activeWorkspace, activeApiSpec) => {
if (!activeWorkspace) {
// see above, but since the selectActiveWorkspace selector really can return undefined, we need to handle it here.
return undefined;
}
return isCollection(activeWorkspace) ? activeWorkspace.name : activeApiSpec?.fileName;
}
);
export const selectEnvironments = createSelector(
selectEntitiesLists,
entities => entities.environments,
);
export const selectGitRepositories = createSelector(
selectEntitiesLists,
entities => entities.gitRepositories,
@@ -227,22 +131,16 @@ export const selectRequests = createSelector(
export const selectActiveEnvironment = createSelector(
selectActiveWorkspaceMeta,
selectEnvironments,
(meta, environments) => {
selectEntitiesLists,
(meta, entities) => {
if (!meta) {
return null;
}
return environments.find(environment => environment._id === meta.activeEnvironmentId) || null;
return entities.environments.find(environment => environment._id === meta.activeEnvironmentId) || null;
},
);
export const selectActiveWorkspaceClientCertificates = createSelector(
selectEntitiesLists,
selectActiveWorkspace,
(entities, activeWorkspace) => entities.clientCertificates.filter(c => c.parentId === activeWorkspace?._id),
);
export const selectActiveGitRepository = createSelector(
selectEntitiesLists,
selectActiveWorkspaceMeta,
@@ -333,7 +231,7 @@ export const selectWorkspaceRequestsAndRequestGroups = createSelector(
);
export const selectActiveRequest = createSelector(
selectEntities,
(state: RootState) => state.entities,
selectActiveWorkspaceMeta,
(entities, workspaceMeta) => {
const id = workspaceMeta?.activeRequestId || 'n/a';
@@ -354,25 +252,6 @@ export const selectActiveRequest = createSelector(
},
);
export const selectActiveCookieJar = createSelector(
selectEntitiesLists,
selectActiveWorkspace,
(entities, workspace) => {
const cookieJar = entities.cookieJars.find(cj => cj.parentId === workspace?._id);
return cookieJar || null;
},
);
export const selectUnseenWorkspaces = createSelector(
selectEntitiesLists,
entities => {
const { workspaces, workspaceMetas } = entities;
return workspaces.filter(workspace => {
const meta = workspaceMetas.find(m => m.parentId === workspace._id);
return !!(meta && !meta.hasSeen);
});
});
export const selectActiveRequestMeta = createSelector(
selectActiveRequest,
selectEntitiesLists,
@@ -449,84 +328,7 @@ export const selectActiveResponse = createSelector(
},
);
export const selectActiveUnitTestResult = createSelector(
selectEntitiesLists,
selectActiveWorkspace,
(entities, activeWorkspace) => {
if (!activeWorkspace) {
return null;
}
let recentResult: UnitTestResult | null = null;
for (const r of entities.unitTestResults) {
if (r.parentId !== activeWorkspace._id) {
continue;
}
if (!recentResult) {
recentResult = r;
continue;
}
if (r.created > recentResult.created) {
recentResult = r;
}
}
return recentResult;
},
);
export const selectActiveUnitTestSuite = createSelector(
selectEntitiesLists,
selectActiveWorkspaceMeta,
(entities, activeWorkspaceMeta) => {
if (!activeWorkspaceMeta) {
return null;
}
const id = activeWorkspaceMeta.activeUnitTestSuiteId;
return entities.unitTestSuites.find(s => s._id === id) || null;
},
);
export const selectActiveUnitTests = createSelector(
selectEntitiesLists,
selectActiveUnitTestSuite,
(entities, activeUnitTestSuite) => {
if (!activeUnitTestSuite) {
return [];
}
return entities.unitTests.filter(s => s.parentId === activeUnitTestSuite._id);
},
);
export const selectActiveProjectName = createSelector(
selectActiveProject,
activeProject => activeProject.name,
);
export const selectActiveUnitTestSuites = createSelector(
selectEntitiesLists,
selectActiveWorkspace,
(entities, activeWorkspace) => {
return entities.unitTestSuites.filter(s => s.parentId === activeWorkspace?._id);
},
);
export const selectSyncItems = createSelector(
selectActiveWorkspaceEntities,
getStatusCandidates,
);
export const selectIsLoggedIn = createSelector(
selectGlobal,
global => global.isLoggedIn,
);
export const selectActiveActivity = createSelector(
selectGlobal,
global => global.activeActivity,
);

View File

@@ -1,6 +1,5 @@
import { createSelector } from 'reselect';
import { DEFAULT_PANE_HEIGHT, DEFAULT_PANE_WIDTH, DEFAULT_SIDEBAR_WIDTH } from '../../common/constants';
import { fuzzyMatchAll } from '../../common/misc';
import type { BaseModel } from '../../models';
import { GrpcRequest, isGrpcRequest } from '../../models/grpc-request';
@@ -43,27 +42,10 @@ export interface SidebarChildren {
all: Child[];
pinned: Child[];
}
export const selectSidebarWidth = createSelector(
selectActiveWorkspaceMeta,
activeWorkspaceMeta => activeWorkspaceMeta?.sidebarWidth || DEFAULT_SIDEBAR_WIDTH,
);
export const selectPaneWidth = createSelector(
selectActiveWorkspaceMeta,
activeWorkspaceMeta => activeWorkspaceMeta?.paneWidth || DEFAULT_PANE_WIDTH,
);
export const selectPaneHeight = createSelector(
selectActiveWorkspaceMeta,
activeWorkspaceMeta => activeWorkspaceMeta?.paneHeight || DEFAULT_PANE_HEIGHT,
);
export const selectSidebarFilter = createSelector(
selectActiveWorkspaceMeta,
activeWorkspaceMeta => activeWorkspaceMeta ? activeWorkspaceMeta.sidebarFilter : '',
);
export const selectSidebarChildren = createSelector(
selectCollapsedRequestGroups,
selectPinnedRequests,

View File

@@ -1,6 +1,7 @@
import { ServiceError, StatusObject } from '@grpc/grpc-js';
import React, { FC, Fragment, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { ChangeBufferEvent, database as db } from '../../common/database';
import { generateId } from '../../common/misc';
@@ -39,13 +40,10 @@ import { WebSocketRequestPane } from '../components/websockets/websocket-request
import { updateRequestMetaByParentId } from '../hooks/create-request';
import { createRequestGroup } from '../hooks/create-request-group';
import {
selectActiveEnvironment,
selectActiveRequest,
selectActiveWorkspace,
selectActiveWorkspaceMeta,
selectSettings,
} from '../redux/selectors';
import { selectSidebarFilter } from '../redux/sidebar-selectors';
import { WorkspaceLoaderData } from './workspace';
export interface GrpcMessage {
id: string;
text: string;
@@ -74,9 +72,12 @@ const INITIAL_GRPC_REQUEST_STATE = {
};
export const Debug: FC = () => {
const activeEnvironment = useSelector(selectActiveEnvironment);
const {
activeWorkspace,
activeWorkspaceMeta,
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const activeRequest = useSelector(selectActiveRequest);
const activeWorkspace = useSelector(selectActiveWorkspace);
const [grpcStates, setGrpcStates] = useState<GrpcRequestState[]>([]);
useEffect(() => {
db.onChange(async (changes: ChangeBufferEvent[]) => {
@@ -90,18 +91,14 @@ export const Debug: FC = () => {
}, []);
useEffect(() => {
const fn = async () => {
if (activeWorkspace) {
const children = await db.withDescendants(activeWorkspace);
const grpcRequests = children.filter(d => isGrpcRequest(d));
setGrpcStates(grpcRequests.map(r => ({ requestId: r._id, ...INITIAL_GRPC_REQUEST_STATE })));
}
const children = await db.withDescendants(activeWorkspace);
const grpcRequests = children.filter(d => isGrpcRequest(d));
setGrpcStates(grpcRequests.map(r => ({ requestId: r._id, ...INITIAL_GRPC_REQUEST_STATE })));
};
fn();
}, [activeWorkspace]);
const settings = useSelector(selectSettings);
const sidebarFilter = useSelector(selectSidebarFilter);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const [runningRequests, setRunningRequests] = useState({});
const setLoading = (isLoading: boolean) => {
invariant(activeRequest, 'No active request');
@@ -192,27 +189,21 @@ export const Debug: FC = () => {
},
request_createHTTP:
async () => {
if (activeWorkspace) {
const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id;
const request = await models.request.create({
parentId,
name: 'New Request',
});
if (activeWorkspaceMeta) {
await models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: request._id });
}
await updateRequestMetaByParentId(request._id, {
lastActive: Date.now(),
});
models.stats.incrementCreatedRequests();
window.main.trackSegmentEvent({ event: SegmentEvent.requestCreate, properties: { requestType: 'HTTP' } });
}
const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id;
const request = await models.request.create({
parentId,
name: 'New Request',
});
await models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: request._id });
await updateRequestMetaByParentId(request._id, {
lastActive: Date.now(),
});
models.stats.incrementCreatedRequests();
window.main.trackSegmentEvent({ event: SegmentEvent.requestCreate, properties: { requestType: 'HTTP' } });
},
request_showCreateFolder:
() => {
if (activeWorkspace) {
createRequestGroup(activeRequest ? activeRequest.parentId : activeWorkspace._id);
}
createRequestGroup(activeRequest ? activeRequest.parentId : activeWorkspace._id);
},
request_showRecent:
() => showModal(RequestSwitcherModal, {
@@ -263,11 +254,11 @@ export const Debug: FC = () => {
<SidebarFilter
key={`${activeWorkspace._id}::filter`}
filter={sidebarFilter || ''}
filter={activeWorkspaceMeta.sidebarFilter || ''}
/>
<SidebarChildren
filter={sidebarFilter || ''}
filter={activeWorkspaceMeta.sidebarFilter || ''}
/>
<WorkspaceSyncDropdown />
</Fragment>

View File

@@ -1,5 +1,5 @@
import React, { FC, Fragment } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { ErrorBoundary } from '../components/error-boundary';
import { registerModal } from '../components/modals';
@@ -34,17 +34,11 @@ import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-e
import { WorkspaceSettingsModal } from '../components/modals/workspace-settings-modal';
import { WrapperModal } from '../components/modals/wrapper-modal';
import { useVCS } from '../hooks/use-vcs';
import {
selectActiveCookieJar,
selectActiveEnvironment,
selectActiveWorkspace,
} from '../redux/selectors';
import { WorkspaceLoaderData } from './workspace';
const Modals: FC = () => {
const activeCookieJar = useSelector(selectActiveCookieJar);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeEnvironment = useSelector(selectActiveEnvironment);
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
const { activeWorkspace, activeEnvironment, activeCookieJar } = workspaceData || {};
const vcs = useVCS({
workspaceId: activeWorkspace?._id,
});
@@ -113,6 +107,10 @@ const Modals: FC = () => {
registerModal(instance, 'WorkspaceSettingsModal')
}
/>
<RequestSwitcherModal
ref={instance => registerModal(instance, 'RequestSwitcherModal')}
/>
</>
) : null}
@@ -128,10 +126,6 @@ const Modals: FC = () => {
ref={instance => registerModal(instance, 'ResponseDebugModal')}
/>
<RequestSwitcherModal
ref={instance => registerModal(instance, 'RequestSwitcherModal')}
/>
<EnvironmentEditModal
ref={instance => registerModal(instance, 'EnvironmentEditModal')}
/>

View File

@@ -2,6 +2,9 @@ import React from 'react';
import { LoaderFunction, Outlet, useLoaderData } from 'react-router-dom';
import * as models from '../../models';
import { ApiSpec } from '../../models/api-spec';
import { ClientCertificate } from '../../models/client-certificate';
import { CookieJar } from '../../models/cookie-jar';
import { Environment } from '../../models/environment';
import { GitRepository } from '../../models/git-repository';
import { Project } from '../../models/project';
@@ -10,12 +13,15 @@ import { WorkspaceMeta } from '../../models/workspace-meta';
import { invariant } from '../../utils/invariant';
export interface WorkspaceLoaderData {
activeWorkspace: Workspace;
activeWorkspaceMeta?: WorkspaceMeta;
activeWorkspaceMeta: WorkspaceMeta;
activeProject: Project;
gitRepository: GitRepository | null;
activeEnvironment: Environment;
activeCookieJar: CookieJar;
baseEnvironment: Environment;
subEnvironments: Environment[];
activeApiSpec: ApiSpec | null;
clientCertificates: ClientCertificate[];
}
export const workspaceLoader: LoaderFunction = async ({
@@ -38,6 +44,7 @@ export const workspaceLoader: LoaderFunction = async ({
const activeWorkspaceMeta = await models.workspaceMeta.getOrCreateByParentId(
workspaceId,
);
invariant(activeWorkspaceMeta, 'Workspace meta not found');
const gitRepository = await models.gitRepository.getById(
activeWorkspaceMeta.gitRepositoryId || '',
);
@@ -50,20 +57,28 @@ export const workspaceLoader: LoaderFunction = async ({
const activeEnvironment = subEnvironments.find(({ _id }) => activeWorkspaceMeta.activeEnvironmentId === _id) || baseEnvironment;
const activeCookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
invariant(activeCookieJar, 'Cookie jar not found');
const activeApiSpec = await models.apiSpec.getByParentId(workspaceId);
const clientCertificates = await models.clientCertificate.findByParentId(workspaceId);
return {
activeWorkspace,
activeProject,
gitRepository,
activeWorkspaceMeta,
activeCookieJar,
activeEnvironment,
subEnvironments,
baseEnvironment,
activeApiSpec,
clientCertificates,
};
};
const WorkspaceRoute = () => {
const workspaceData = useLoaderData() as WorkspaceLoaderData;
const branch = workspaceData.activeWorkspaceMeta?.cachedGitRepositoryBranch;
const branch = workspaceData.activeWorkspaceMeta.cachedGitRepositoryBranch;
return <Outlet key={branch} />;
};