mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-25 08:09:06 -04:00
Pull a project into the correct space (#3873)
* use selector and improve types * Loading remote projects now requires a team id; it is no longer optional * activate with workspace or workspace id * extract helper * normalize graphql return type for a remote project to include the team * pull into the space as defined by the project team id, not by the active space. If the space doesn't exist, create it * use helper * make testing easier by putting the active space name as the first breadcrumb * fix tests * add the active space into the title * refactor and add tests * remove todo
This commit is contained in:
1 parent
e683a02b16
commit
be2c347cf1
24 files changed
+363
-116
No files matched your search
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MockStoreEnhanced } from 'redux-mock-store';
|
||||
|
||||
import { RootState } from '../ui/redux/modules';
|
||||
|
||||
export const withReduxStore = (store: MockStoreEnhanced<RootState, {}>) =>
|
||||
class ReduxWrapper extends React.Component {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
};
|
||||
@@ -11,18 +11,25 @@ export const canSync = false;
|
||||
|
||||
export const BASE_SPACE_ID = `${prefix}_base-space`;
|
||||
|
||||
export const isBaseSpace = ({ _id }: Space) => _id === BASE_SPACE_ID;
|
||||
export const isBaseSpace = (space: Space) => space._id === BASE_SPACE_ID;
|
||||
export const isNotBaseSpace = (space: Space) => !isBaseSpace(space);
|
||||
export const isLocalSpace = ({ remoteId }: Space) => remoteId === null;
|
||||
export const isRemoteSpace = (space: Space) => !isLocalSpace(space);
|
||||
export const isLocalSpace = (space: Space): space is LocalSpace => space.remoteId === null;
|
||||
export const isRemoteSpace = (space: Space): space is RemoteSpace => !isLocalSpace(space);
|
||||
export const spaceHasSettings = (space: Space) => isLocalSpace(space) && !isBaseSpace(space);
|
||||
|
||||
interface BaseSpace {
|
||||
interface CommonSpace {
|
||||
name: string;
|
||||
remoteId: string | null;
|
||||
}
|
||||
|
||||
export type Space = BaseModel & BaseSpace;
|
||||
export interface RemoteSpace extends BaseModel, CommonSpace {
|
||||
remoteId: string;
|
||||
}
|
||||
|
||||
export interface LocalSpace extends BaseModel, CommonSpace {
|
||||
remoteId: null;
|
||||
}
|
||||
|
||||
export type Space = LocalSpace | RemoteSpace;
|
||||
|
||||
export const isSpace = (model: Pick<BaseModel, 'type'>): model is Space => (
|
||||
model.type === type
|
||||
@@ -32,7 +39,7 @@ export const isSpaceId = (id: string | null) => (
|
||||
id?.startsWith(`${prefix}_`)
|
||||
);
|
||||
|
||||
export function init(): BaseSpace {
|
||||
export function init(): Partial<Space> {
|
||||
return {
|
||||
name: 'My Space',
|
||||
remoteId: null, // `null` is necessary for the model init logic to work properly
|
||||
@@ -55,6 +62,10 @@ export function getById(_id: string) {
|
||||
return db.getWhere<Space>(type, { _id });
|
||||
}
|
||||
|
||||
export function getByRemoteId(remoteId: string) {
|
||||
return db.getWhere<Space>(type, { remoteId });
|
||||
}
|
||||
|
||||
export function remove(obj: Space) {
|
||||
return db.remove(obj);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createBuilder, Schema } from '@develohpanda/fluent-builder';
|
||||
|
||||
import { baseModelSchema } from '../../models/__schemas__/model-schemas';
|
||||
import { Branch, MergeConflict, Project, SnapshotStateEntry, StatusCandidate } from '../types';
|
||||
import { Branch, MergeConflict, Project, SnapshotStateEntry, StatusCandidate, Team } from '../types';
|
||||
import { ProjectWithTeam } from '../vcs/normalize-project-team';
|
||||
|
||||
export const projectSchema: Schema<Project> = {
|
||||
id: () => 'id',
|
||||
@@ -9,6 +10,16 @@ export const projectSchema: Schema<Project> = {
|
||||
name: () => 'name',
|
||||
};
|
||||
|
||||
export const teamSchema: Schema<Team> = {
|
||||
id: () => 'teamId',
|
||||
name: () => 'teamName',
|
||||
};
|
||||
|
||||
export const projectWithTeamSchema: Schema<ProjectWithTeam> = {
|
||||
...projectSchema,
|
||||
team: () => createBuilder(teamSchema).build(),
|
||||
};
|
||||
|
||||
export const branchSchema: Schema<Branch> = {
|
||||
created: () => new Date(0),
|
||||
modified: () => new Date(0),
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createBuilder } from '@develohpanda/fluent-builder';
|
||||
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import * as models from '../../../models';
|
||||
import { isRemoteSpace, Space } from '../../../models/space';
|
||||
import { isCollection, Workspace, WorkspaceScopeKeys } from '../../../models/workspace';
|
||||
import { projectSchema, teamSchema } from '../../__schemas__/type-schemas';
|
||||
import { initializeSpaceFromTeam, initializeWorkspaceFromProject } from '../initialize-model-from';
|
||||
|
||||
const teamBuilder = createBuilder(teamSchema);
|
||||
const projectBuilder = createBuilder(projectSchema);
|
||||
|
||||
describe('initializeSpaceFromTeam', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('should initialize space with properties from the team', async () => {
|
||||
// Arrange
|
||||
const team = teamBuilder.build();
|
||||
|
||||
// Act
|
||||
const space = await initializeSpaceFromTeam(team);
|
||||
|
||||
// Assert
|
||||
expect(isRemoteSpace(space)).toBe(true);
|
||||
expect(space).toMatchObject<Partial<Space>>({
|
||||
_id:`${models.space.prefix}_${team.id}`,
|
||||
remoteId: team.id,
|
||||
name: team.name,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeWorkspaceFromProject', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('should initialize workspac with properties from the project and space', async () => {
|
||||
// Arrange
|
||||
const space = await models.space.create();
|
||||
const project = projectBuilder.build();
|
||||
|
||||
// Act
|
||||
const workspace = await initializeWorkspaceFromProject(project, space);
|
||||
|
||||
// Assert
|
||||
expect(isCollection(workspace)).toBe(true);
|
||||
expect(workspace).toMatchObject<Partial<Workspace>>({
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
parentId: space._id,
|
||||
scope: WorkspaceScopeKeys.collection,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,15 +4,16 @@ import { mocked } from 'ts-jest/utils';
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import { DEFAULT_BRANCH_NAME } from '../../../common/constants';
|
||||
import * as models from '../../../models';
|
||||
import { isRemoteSpace } from '../../../models/space';
|
||||
import { Workspace } from '../../../models/workspace';
|
||||
import { projectSchema } from '../../__schemas__/type-schemas';
|
||||
import { projectWithTeamSchema } from '../../__schemas__/type-schemas';
|
||||
import MemoryDriver from '../../store/drivers/memory-driver';
|
||||
import { pullProject } from '../pull-project';
|
||||
import { VCS } from '../vcs';
|
||||
|
||||
jest.mock('../vcs');
|
||||
|
||||
const project = createBuilder(projectSchema).build();
|
||||
const project = createBuilder(projectWithTeamSchema).build();
|
||||
|
||||
const newMockedVcs = () => mocked(new VCS(new MemoryDriver()), true);
|
||||
|
||||
@@ -39,14 +40,19 @@ describe('pullProject()', () => {
|
||||
expect(vcs.pull).not.toHaveBeenCalledWith([], expect.anything());
|
||||
});
|
||||
|
||||
it('should insert a workspace with parent', async () => {
|
||||
it('should use existing space', async () => {
|
||||
// Arrange
|
||||
const space = await models.space.create();
|
||||
const space = await models.space.create({
|
||||
name: `${project.team.name} unique`,
|
||||
remoteId: project.team.id,
|
||||
});
|
||||
|
||||
// Act
|
||||
await pullProject({ vcs, project, space });
|
||||
await pullProject({ vcs, project, remoteSpaces: [space].filter(isRemoteSpace) });
|
||||
|
||||
// Assert
|
||||
expect(space?.name).not.toBe(project.team.name); // should not rename if the space already exists
|
||||
|
||||
const workspaces = await models.workspace.all();
|
||||
expect(workspaces).toHaveLength(1);
|
||||
const workspace = workspaces[0];
|
||||
@@ -54,25 +60,46 @@ describe('pullProject()', () => {
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
parentId: space._id,
|
||||
scope: 'collection',
|
||||
}));
|
||||
});
|
||||
|
||||
it('should insert a space and workspace with parent', async () => {
|
||||
// Act
|
||||
await pullProject({ vcs, project, remoteSpaces: [] });
|
||||
|
||||
// Assert
|
||||
const space = await models.space.getByRemoteId(project.team.id);
|
||||
expect(space?.name).toBe(project.team.name);
|
||||
|
||||
const workspaces = await models.workspace.all();
|
||||
expect(workspaces).toHaveLength(1);
|
||||
const workspace = workspaces[0];
|
||||
expect(workspace).toStrictEqual(expect.objectContaining<Partial<Workspace>>({
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
parentId: space?._id,
|
||||
scope: 'collection',
|
||||
}));
|
||||
});
|
||||
|
||||
it('should update a workspace if the name or parentId is different', async () => {
|
||||
// Arrange
|
||||
await models.workspace.create({ _id: project.rootDocumentId, name: 'someName' });
|
||||
const space = await models.space.create();
|
||||
|
||||
// Act
|
||||
await pullProject({ vcs, project, space });
|
||||
await pullProject({ vcs, project, remoteSpaces: [] });
|
||||
|
||||
// Assert
|
||||
const space = await models.space.getByRemoteId(project.team.id);
|
||||
|
||||
const workspaces = await models.workspace.all();
|
||||
expect(workspaces).toHaveLength(1);
|
||||
const workspace = workspaces[0];
|
||||
expect(workspace).toStrictEqual(expect.objectContaining<Partial<Workspace>>({
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
parentId: space._id,
|
||||
parentId: space?._id,
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -87,23 +114,24 @@ describe('pullProject()', () => {
|
||||
|
||||
it('should overwrite the parentId only for a workspace with the space id', async () => {
|
||||
// Arrange
|
||||
const space = await models.space.create({ remoteId: 'abc' });
|
||||
const existingWrk = await models.workspace.create({ _id: project.rootDocumentId, name: project.name });
|
||||
const existingReq = await models.request.create({ parentId: existingWrk._id });
|
||||
|
||||
vcs.allDocuments.mockResolvedValue([existingWrk, existingReq]);
|
||||
|
||||
// Act
|
||||
await pullProject({ vcs, project, space });
|
||||
await pullProject({ vcs, project, remoteSpaces: [] });
|
||||
|
||||
// Assert
|
||||
const space = await models.space.getByRemoteId(project.team.id);
|
||||
|
||||
const workspaces = await models.workspace.all();
|
||||
expect(workspaces).toHaveLength(1);
|
||||
const workspace = workspaces[0];
|
||||
expect(workspace).toStrictEqual(expect.objectContaining<Partial<Workspace>>({
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
parentId: space._id,
|
||||
parentId: space?._id,
|
||||
}));
|
||||
|
||||
const requests = await models.request.all();
|
||||
@@ -111,7 +139,7 @@ describe('pullProject()', () => {
|
||||
const request = requests[0];
|
||||
expect(request).toStrictEqual(existingReq);
|
||||
|
||||
expect(vcs.pull).toHaveBeenCalledWith([], space.remoteId);
|
||||
expect(vcs.pull).toHaveBeenCalledWith([], space?.remoteId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as models from '../../models';
|
||||
import { RemoteSpace } from '../../models/space';
|
||||
import { Space } from '../../models/space';
|
||||
import { Workspace, WorkspaceScopeKeys } from '../../models/workspace';
|
||||
import { Team } from '../types';
|
||||
import { Project } from '../types';
|
||||
|
||||
export const initializeSpaceFromTeam = (team: Team) => models.initModel<RemoteSpace>(
|
||||
models.space.type,
|
||||
{
|
||||
_id: `${models.space.prefix}_${team.id}`,
|
||||
remoteId: team.id,
|
||||
name: team.name,
|
||||
}
|
||||
);
|
||||
|
||||
export const initializeWorkspaceFromProject = (project: Project, space: Space) => models.initModel<Workspace>(
|
||||
models.workspace.type,
|
||||
{
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
parentId: space._id,
|
||||
scope: WorkspaceScopeKeys.collection,
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Project, Team } from '../types';
|
||||
|
||||
export interface ProjectWithTeams extends Project {
|
||||
teams: Team[];
|
||||
}
|
||||
|
||||
export interface ProjectWithTeam extends Project {
|
||||
team: Team;
|
||||
}
|
||||
|
||||
export const normalizeProjectTeam = (project: ProjectWithTeams): ProjectWithTeam => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
rootDocumentId: project.rootDocumentId,
|
||||
// A project is guaranteed to exist on exactly one team
|
||||
team: project.teams[0],
|
||||
});
|
||||
@@ -1,34 +1,35 @@
|
||||
|
||||
import { DEFAULT_BRANCH_NAME } from '../../common/constants';
|
||||
import { database } from '../../common/database';
|
||||
import * as models from '../../models';
|
||||
import { Space } from '../../models/space';
|
||||
import { isWorkspace, Workspace } from '../../models/workspace';
|
||||
import { Project } from '../types';
|
||||
import { RemoteSpace } from '../../models/space';
|
||||
import { isWorkspace } from '../../models/workspace';
|
||||
import { initializeSpaceFromTeam, initializeWorkspaceFromProject } from './initialize-model-from';
|
||||
import { ProjectWithTeam } from './normalize-project-team';
|
||||
import { VCS } from './vcs';
|
||||
|
||||
interface Options {
|
||||
vcs: VCS;
|
||||
project: Project;
|
||||
space: Space;
|
||||
project: ProjectWithTeam;
|
||||
remoteSpaces: RemoteSpace[];
|
||||
}
|
||||
|
||||
export const pullProject = async ({ vcs, project, space }: Options) => {
|
||||
export const pullProject = async ({ vcs, project, remoteSpaces }: Options) => {
|
||||
// Set project, checkout master, and pull
|
||||
await vcs.setProject(project);
|
||||
await vcs.checkout([], DEFAULT_BRANCH_NAME);
|
||||
const remoteBranches = await vcs.getRemoteBranches();
|
||||
const defaultBranchMissing = !remoteBranches.includes(DEFAULT_BRANCH_NAME);
|
||||
|
||||
const workspaceParentId = space._id;
|
||||
// Find or create the remote space locally
|
||||
let space = remoteSpaces.find(({ remoteId }) => remoteId === project.team.id);
|
||||
if (!space) {
|
||||
space = await initializeSpaceFromTeam(project.team);
|
||||
await database.upsert(space);
|
||||
}
|
||||
|
||||
// The default branch does not exist, so we create it and the workspace locally
|
||||
if (defaultBranchMissing) {
|
||||
const workspace: Workspace = await models.initModel(models.workspace.type, {
|
||||
_id: project.rootDocumentId,
|
||||
name: project.name,
|
||||
parentId: workspaceParentId,
|
||||
});
|
||||
const workspace = await initializeWorkspaceFromProject(project, space);
|
||||
await database.upsert(workspace);
|
||||
} else {
|
||||
await vcs.pull([], space.remoteId); // There won't be any existing docs since it's a new pull
|
||||
@@ -38,11 +39,13 @@ export const pullProject = async ({ vcs, project, space }: Options) => {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
for (const doc of (await vcs.allDocuments() || [])) {
|
||||
if (isWorkspace(doc)) {
|
||||
doc.parentId = workspaceParentId;
|
||||
doc.parentId = space._id;
|
||||
}
|
||||
await database.upsert(doc);
|
||||
}
|
||||
|
||||
await database.flushChanges(flushId);
|
||||
}
|
||||
|
||||
return space;
|
||||
};
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
StatusCandidate,
|
||||
Team,
|
||||
} from '../types';
|
||||
import { normalizeProjectTeam, ProjectWithTeams } from './normalize-project-team';
|
||||
import * as paths from './paths';
|
||||
import {
|
||||
compareBranches,
|
||||
@@ -121,10 +122,14 @@ export class VCS {
|
||||
return this._allProjects();
|
||||
}
|
||||
|
||||
async remoteProjects(teamId?: string) {
|
||||
async remoteProjects(teamId: string) {
|
||||
return this._queryProjects(teamId);
|
||||
}
|
||||
|
||||
async remoteProjectsInAnyTeam() {
|
||||
return this._queryProjects();
|
||||
}
|
||||
|
||||
async blobFromLastSnapshot(key: string) {
|
||||
const branch = await this._getCurrentBranch();
|
||||
const snapshot = await this._getLatestSnapshot(branch.name);
|
||||
@@ -1113,9 +1118,7 @@ export class VCS {
|
||||
return projectShareInstructions;
|
||||
}
|
||||
|
||||
async _queryProjects(
|
||||
teamId?: string,
|
||||
): Promise<Project[]> {
|
||||
async _queryProjects(teamId?: string) {
|
||||
const { projects } = await this._runGraphQL(
|
||||
`
|
||||
query ($teamId: ID) {
|
||||
@@ -1123,6 +1126,10 @@ export class VCS {
|
||||
id
|
||||
name
|
||||
rootDocumentId
|
||||
teams {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
@@ -1131,7 +1138,8 @@ export class VCS {
|
||||
},
|
||||
'projects',
|
||||
);
|
||||
return projects;
|
||||
|
||||
return (projects as ProjectWithTeams[]).map(normalizeProjectTeam);
|
||||
}
|
||||
|
||||
async _queryProject(): Promise<Project | null> {
|
||||
|
||||
@@ -9,15 +9,9 @@ export const getAndClearShowPromptMockArgs = () => {
|
||||
|
||||
export const getAndClearShowAlertMockArgs = () => {
|
||||
const mockFn = showAlert as jest.Mock<typeof showAlert, Parameters<typeof showAlert>>;
|
||||
const { title, okLabel, addCancel, message, onConfirm } = mockFn.mock.calls[0][0];
|
||||
const options = mockFn.mock.calls[0][0];
|
||||
mockFn.mockClear();
|
||||
return {
|
||||
title,
|
||||
okLabel,
|
||||
addCancel,
|
||||
message,
|
||||
onConfirm,
|
||||
};
|
||||
return options;
|
||||
};
|
||||
|
||||
export const getAndClearShowErrorMockArgs = () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import * as session from '../../../account/session';
|
||||
import { AUTOBIND_CFG, DEFAULT_BRANCH_NAME } from '../../../common/constants';
|
||||
@@ -8,13 +10,17 @@ import { database as db } from '../../../common/database';
|
||||
import { docsVersionControl } from '../../../common/documentation';
|
||||
import { strings } from '../../../common/strings';
|
||||
import * as models from '../../../models';
|
||||
import { Space } from '../../../models/space';
|
||||
import { isRemoteSpace, Space } from '../../../models/space';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../../models/workspace-meta';
|
||||
import type { Project, Snapshot, Status, StatusCandidate } from '../../../sync/types';
|
||||
import { Snapshot, Status, StatusCandidate } from '../../../sync/types';
|
||||
import { pushSnapshotOnInitialize } from '../../../sync/vcs/initialize-project';
|
||||
import { ProjectWithTeam } from '../../../sync/vcs/normalize-project-team';
|
||||
import { pullProject } from '../../../sync/vcs/pull-project';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { RootState } from '../../redux/modules';
|
||||
import { activateWorkspace } from '../../redux/modules/workspace';
|
||||
import { selectRemoteSpaces } from '../../redux/selectors';
|
||||
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown';
|
||||
import Link from '../base/link';
|
||||
import PromptButton from '../base/prompt-button';
|
||||
@@ -33,7 +39,20 @@ const REFRESH_USER_ACTIVITY = 1000 * 60 * 10;
|
||||
// Refresh dropdown periodically
|
||||
const REFRESH_PERIOD = 1000 * 60 * 1;
|
||||
|
||||
interface Props {
|
||||
type ReduxProps = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>;
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
remoteSpaces: selectRemoteSpaces(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
const bound = bindActionCreators({ activateWorkspace }, dispatch);
|
||||
return {
|
||||
handleActivateWorkspace: bound.activateWorkspace,
|
||||
};
|
||||
};
|
||||
|
||||
interface Props extends ReduxProps {
|
||||
workspace: Workspace;
|
||||
workspaceMeta?: WorkspaceMeta;
|
||||
space: Space;
|
||||
@@ -55,11 +74,11 @@ interface State {
|
||||
loadingPull: boolean;
|
||||
loadingProjectPull: boolean;
|
||||
loadingPush: boolean;
|
||||
remoteProjects: Project[];
|
||||
remoteProjects: ProjectWithTeam[];
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
class SyncDropdown extends PureComponent<Props, State> {
|
||||
class UnconnectedSyncDropdown extends PureComponent<Props, State> {
|
||||
checkInterval: NodeJS.Timeout | null = null;
|
||||
refreshOnNextSyncItems = false;
|
||||
lastUserActivity = Date.now();
|
||||
@@ -85,10 +104,10 @@ class SyncDropdown extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
async refreshMainAttributes(extraState: Partial<State> = {}) {
|
||||
const { vcs, syncItems, workspace } = this.props;
|
||||
const { vcs, syncItems, workspace, space } = this.props;
|
||||
|
||||
if (!vcs.hasProject()) {
|
||||
const remoteProjects = await vcs.remoteProjects();
|
||||
if (!vcs.hasProject() && isRemoteSpace(space)) {
|
||||
const remoteProjects = await vcs.remoteProjectsInAnyTeam();
|
||||
const matchedProjects = remoteProjects.filter(p => p.rootDocumentId === workspace._id);
|
||||
this.setState({
|
||||
remoteProjects: matchedProjects,
|
||||
@@ -292,13 +311,21 @@ class SyncDropdown extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
async _handleSetProject(p: Project) {
|
||||
const { vcs, space } = this.props;
|
||||
async _handleSetProject(p: ProjectWithTeam) {
|
||||
const { vcs, remoteSpaces, space, workspace, handleActivateWorkspace } = this.props;
|
||||
this.setState({
|
||||
loadingProjectPull: true,
|
||||
});
|
||||
|
||||
await pullProject({ vcs, project: p, space });
|
||||
const pulledIntoSpace = await pullProject({ vcs, project: p, remoteSpaces });
|
||||
if (pulledIntoSpace._id !== space._id) {
|
||||
// If pulled into a different space, reactivate the workspace
|
||||
await handleActivateWorkspace({ workspaceId: workspace._id });
|
||||
console.log('[sync] collection has been moved to the remote space to which it belongs', {
|
||||
id: pulledIntoSpace._id,
|
||||
name: pulledIntoSpace.name,
|
||||
});
|
||||
}
|
||||
|
||||
await this.refreshMainAttributes({
|
||||
loadingProjectPull: false,
|
||||
@@ -583,4 +610,5 @@ class SyncDropdown extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const SyncDropdown = connect(mapStateToProps, mapDispatchToProps)(UnconnectedSyncDropdown);
|
||||
export default SyncDropdown;
|
||||
@@ -174,7 +174,7 @@ class RequestSwitcherModal extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
async _activateWorkspace(workspace: Workspace) {
|
||||
await this.props.handleActivateWorkspace(workspace);
|
||||
await this.props.handleActivateWorkspace({ workspace });
|
||||
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ const WorkspaceDuplicateModalInternalWithRef: ForwardRefRenderFunction<Modal, In
|
||||
await initializeLocalProjectAndMarkForSync({ vcs: vcs.newInstance(), workspace: newWorkspace });
|
||||
}
|
||||
|
||||
dispatch(activateWorkspace(newWorkspace));
|
||||
dispatch(activateWorkspace({ workspace: newWorkspace }));
|
||||
hide();
|
||||
onDone?.();
|
||||
}, [dispatch, hide, isLoggedIn, onDone, spaces, vcs, workspace]);
|
||||
|
||||
@@ -54,15 +54,18 @@ const WorkspacePageHeader: FunctionComponent<Props> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const crumbs = [
|
||||
{ id: 'home', node: strings.home.singular, onClick: homeCallback },
|
||||
{ id: 'workspace', node: <Fragment key="workspace-dd">{workspace}</Fragment> },
|
||||
];
|
||||
|
||||
return (
|
||||
<Header
|
||||
className="app-header theme--app-header"
|
||||
gridLeft={
|
||||
<Fragment>
|
||||
<img src={coreLogo} alt="Insomnia" width="24" height="24" />
|
||||
<Breadcrumb
|
||||
crumbs={[{ id: 'home', node: strings.home.singular, onClick: homeCallback },
|
||||
{ id: 'workspace', node: <Fragment key="workspace-dd">{workspace}</Fragment> }]}/>
|
||||
<Breadcrumb crumbs={crumbs}/>
|
||||
</Fragment>
|
||||
}
|
||||
gridCenter={
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AUTOBIND_CFG, GlobalActivity, SortOrder } from '../../common/constants'
|
||||
import { isGrpcRequest } from '../../models/grpc-request';
|
||||
import { Request, RequestAuthentication, RequestBody, RequestHeader, RequestParameter } from '../../models/request';
|
||||
import { Settings } from '../../models/settings';
|
||||
import { isRemoteSpace } from '../../models/space';
|
||||
import { isCollection, isDesign } from '../../models/workspace';
|
||||
import EnvironmentsDropdown from './dropdowns/environments-dropdown';
|
||||
import SyncDropdown from './dropdowns/sync-dropdown';
|
||||
@@ -66,7 +67,7 @@ class WrapperDebug extends PureComponent<Props> {
|
||||
|
||||
let insomniaSync: ReactNode = null;
|
||||
|
||||
if (isLoggedIn && collection && activeSpace?.remoteId && vcs) {
|
||||
if (isLoggedIn && collection && isRemoteSpace(activeSpace) && vcs) {
|
||||
insomniaSync = <SyncDropdown
|
||||
workspace={activeWorkspace}
|
||||
workspaceMeta={activeWorkspaceMeta}
|
||||
|
||||
@@ -341,7 +341,7 @@ class WrapperHome extends PureComponent<Props, State> {
|
||||
{...props}
|
||||
key={props.apiSpec._id}
|
||||
activeSpace={activeSpace}
|
||||
onSelect={() => handleActivateWorkspace(props.workspace)}
|
||||
onSelect={() => handleActivateWorkspace({ workspace: props.workspace })}
|
||||
filter={filter}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -1097,12 +1097,13 @@ class App extends PureComponent<AppProps, State> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update document.title to be "Workspace (Environment) – Request" when not home
|
||||
* Update document.title to be "Space - Workspace (Environment) – Request" when not home
|
||||
* @private
|
||||
*/
|
||||
_updateDocumentTitle() {
|
||||
const {
|
||||
activeWorkspace,
|
||||
activeSpace,
|
||||
activeApiSpec,
|
||||
activeEnvironment,
|
||||
activeRequest,
|
||||
@@ -1113,7 +1114,8 @@ class App extends PureComponent<AppProps, State> {
|
||||
if (activity === ACTIVITY_HOME || activity === ACTIVITY_MIGRATION) {
|
||||
title = getAppName();
|
||||
} else if (activeWorkspace && activeApiSpec) {
|
||||
title = isCollection(activeWorkspace) ? activeWorkspace.name : activeApiSpec.fileName;
|
||||
title = activeSpace.name;
|
||||
title += ` - ${isCollection(activeWorkspace) ? activeWorkspace.name : activeApiSpec.fileName}`;
|
||||
|
||||
if (activeEnvironment) {
|
||||
title += ` (${activeEnvironment.name})`;
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { mocked } from 'ts-jest/utils';
|
||||
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import { isLoggedIn as _isLoggedIn } from '../../../account/session';
|
||||
import { reduxStateForTest } from '../../../__jest__/redux-state-for-test';
|
||||
import { withReduxStore } from '../../../__jest__/with-redux-store';
|
||||
import * as models from '../../../models';
|
||||
import { BASE_SPACE_ID, Space } from '../../../models/space';
|
||||
import MemoryDriver from '../../../sync/store/drivers/memory-driver';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { RootState } from '../../redux/modules';
|
||||
import { useRemoteSpaces } from '../space';
|
||||
|
||||
jest.mock('../../../account/session', () => ({
|
||||
isLoggedIn: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../sync/vcs/vcs');
|
||||
|
||||
const isLoggedIn = mocked(_isLoggedIn);
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureMockStore<RootState>(middlewares);
|
||||
|
||||
const newMockedVcs = () => mocked(new VCS(new MemoryDriver()), true);
|
||||
|
||||
describe('useRemoteSpaces', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('should not load teams if VCS is not set', () => {
|
||||
const { result } = renderHook(() => useRemoteSpaces());
|
||||
it('should not load teams if VCS is not set', async () => {
|
||||
const store = mockStore(await reduxStateForTest());
|
||||
|
||||
const { result } = renderHook(() => useRemoteSpaces(), { wrapper: withReduxStore(store) });
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should not load teams if not signed in', async () => {
|
||||
const store = mockStore(await reduxStateForTest({ isLoggedIn: false }));
|
||||
|
||||
const vcs = newMockedVcs();
|
||||
isLoggedIn.mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useRemoteSpaces(vcs));
|
||||
const { result } = renderHook(() => useRemoteSpaces(vcs), { wrapper: withReduxStore(store) });
|
||||
result.current.refresh();
|
||||
|
||||
expect(vcs.teams).not.toHaveBeenCalled();
|
||||
@@ -40,7 +45,7 @@ describe('useRemoteSpaces', () => {
|
||||
});
|
||||
|
||||
it('should load teams each time VCS changes', async () => {
|
||||
isLoggedIn.mockReturnValue(true);
|
||||
const store = mockStore(await reduxStateForTest({ isLoggedIn: true }));
|
||||
|
||||
const vcs1 = newMockedVcs();
|
||||
const vcs2 = newMockedVcs();
|
||||
@@ -49,7 +54,7 @@ describe('useRemoteSpaces', () => {
|
||||
vcs1.teams.mockResolvedValue([team1]);
|
||||
vcs2.teams.mockResolvedValue([team2]);
|
||||
|
||||
const { result, rerender, waitFor } = renderHook(prop => useRemoteSpaces(prop), { initialProps: vcs1 });
|
||||
const { result, rerender, waitFor } = renderHook((prop: VCS) => useRemoteSpaces(prop), { initialProps: vcs1, wrapper: withReduxStore(store) });
|
||||
|
||||
// Wait for effect
|
||||
await waitFor(() => result.current.loading === true);
|
||||
@@ -83,12 +88,12 @@ describe('useRemoteSpaces', () => {
|
||||
});
|
||||
|
||||
it('should load teams on refresh', async () => {
|
||||
isLoggedIn.mockReturnValue(true);
|
||||
const store = mockStore(await reduxStateForTest({ isLoggedIn: true }));
|
||||
|
||||
const vcs = newMockedVcs();
|
||||
vcs.teams.mockResolvedValue([]);
|
||||
|
||||
const { result, waitFor } = renderHook(() => useRemoteSpaces(vcs));
|
||||
const { result, waitFor } = renderHook(() => useRemoteSpaces(vcs), { wrapper: withReduxStore(store) });
|
||||
|
||||
// Wait for effect
|
||||
await waitFor(() => result.current.loading === true);
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { isLoggedIn } from '../../account/session';
|
||||
import { database } from '../../common/database';
|
||||
import * as models from '../../models';
|
||||
import { Space } from '../../models/space';
|
||||
import { initializeSpaceFromTeam } from '../../sync/vcs/initialize-model-from';
|
||||
import { VCS } from '../../sync/vcs/vcs';
|
||||
import { selectIsLoggedIn } from '../redux/selectors';
|
||||
import { useSafeState } from './use-safe-state';
|
||||
|
||||
export const useRemoteSpaces = (vcs?: VCS) => {
|
||||
const [loading, setLoading] = useSafeState(false);
|
||||
const isLoggedIn = useSelector(selectIsLoggedIn);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (vcs && isLoggedIn()) {
|
||||
if (vcs && isLoggedIn) {
|
||||
setLoading(true);
|
||||
|
||||
const teams = await vcs.teams();
|
||||
const spaces = await Promise.all(teams.map(team => models.initModel<Space>(
|
||||
models.space.type,
|
||||
{
|
||||
_id: `${models.space.prefix}_${team.id}`,
|
||||
remoteId: team.id,
|
||||
name: team.name,
|
||||
},
|
||||
)));
|
||||
const spaces = await Promise.all(teams.map(initializeSpaceFromTeam));
|
||||
await database.batchModifyDocs({ upsert: spaces });
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
}, [vcs, setLoading]);
|
||||
}, [vcs, setLoading, isLoggedIn]);
|
||||
|
||||
// If the refresh callback changes, refresh
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Reducer, useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { isLoggedIn } from '../../account/session';
|
||||
import { isRemoteSpace } from '../../models/space';
|
||||
import { Project } from '../../sync/types';
|
||||
import { ProjectWithTeam } from '../../sync/vcs/normalize-project-team';
|
||||
import { pullProject } from '../../sync/vcs/pull-project';
|
||||
import { VCS } from '../../sync/vcs/vcs';
|
||||
import { showAlert } from '../components/modals';
|
||||
import { selectActiveSpace, selectAllWorkspaces } from '../redux/selectors';
|
||||
import { selectActiveSpace, selectAllWorkspaces, selectIsLoggedIn, selectRemoteSpaces } from '../redux/selectors';
|
||||
import { useSafeReducerDispatch } from './use-safe-reducer-dispatch';
|
||||
|
||||
interface State {
|
||||
loading: boolean;
|
||||
localProjects: Project[];
|
||||
remoteProjects: Project[];
|
||||
remoteProjects: ProjectWithTeam[];
|
||||
pullingProjects: Record<string, boolean>;
|
||||
}
|
||||
|
||||
@@ -48,7 +49,9 @@ export const useRemoteWorkspaces = (vcs?: VCS) => {
|
||||
// Fetch from redux
|
||||
const workspaces = useSelector(selectAllWorkspaces);
|
||||
const activeSpace = useSelector(selectActiveSpace);
|
||||
const spaceRemoteId = activeSpace?.remoteId || undefined;
|
||||
const remoteSpaces = useSelector(selectRemoteSpaces);
|
||||
const isLoggedIn = useSelector(selectIsLoggedIn);
|
||||
|
||||
|
||||
// Local state
|
||||
const [{ loading, localProjects, remoteProjects, pullingProjects }, _dispatch] = useReducer(reducer, initialState);
|
||||
@@ -56,15 +59,15 @@ export const useRemoteWorkspaces = (vcs?: VCS) => {
|
||||
|
||||
// Refresh remote spaces
|
||||
const refresh = useCallback(async () => {
|
||||
if (!vcs || !isLoggedIn()) {
|
||||
if (!vcs || !isLoggedIn || !isRemoteSpace(activeSpace)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'loadProjects' });
|
||||
const remote = await vcs.remoteProjects(spaceRemoteId);
|
||||
const remote = await vcs.remoteProjects(activeSpace.remoteId);
|
||||
const local = await vcs.localProjects();
|
||||
dispatch({ type: 'saveProjects', local, remote });
|
||||
}, [dispatch, spaceRemoteId, vcs]);
|
||||
}, [vcs, isLoggedIn, activeSpace, dispatch]);
|
||||
|
||||
// Find remote spaces that haven't been pulled
|
||||
const missingProjects = useMemo(() => remoteProjects.filter(({ id, rootDocumentId }) => {
|
||||
@@ -77,7 +80,7 @@ export const useRemoteWorkspaces = (vcs?: VCS) => {
|
||||
}), [localProjects, remoteProjects, workspaces]);
|
||||
|
||||
// Pull a remote space
|
||||
const pull = useCallback(async (project: Project) => {
|
||||
const pull = useCallback(async (project: ProjectWithTeam) => {
|
||||
if (!vcs) {
|
||||
throw new Error('VCS is not defined');
|
||||
}
|
||||
@@ -90,7 +93,7 @@ export const useRemoteWorkspaces = (vcs?: VCS) => {
|
||||
// Remove all projects for workspace first
|
||||
await newVCS.removeProjectsForRoot(project.rootDocumentId);
|
||||
|
||||
await pullProject({ vcs: newVCS, project, space: activeSpace });
|
||||
await pullProject({ vcs: newVCS, project, remoteSpaces });
|
||||
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
@@ -101,7 +104,7 @@ export const useRemoteWorkspaces = (vcs?: VCS) => {
|
||||
} finally {
|
||||
dispatch({ type: 'stopPullingProject', projectId: project.id });
|
||||
}
|
||||
}, [vcs, refresh, activeSpace, dispatch]);
|
||||
}, [vcs, refresh, remoteSpaces, dispatch]);
|
||||
|
||||
// If the refresh callback changes, refresh
|
||||
useEffect(() => {
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('git', () => {
|
||||
'No document found in the repository for import. Would you like to create a new one?',
|
||||
);
|
||||
// Confirm
|
||||
await alertArgs.onConfirm();
|
||||
await alertArgs.onConfirm?.();
|
||||
// Should show new design doc prompt
|
||||
const promptArgs = getAndClearShowPromptMockArgs();
|
||||
expect(promptArgs.title).toBe('Create New Design Document');
|
||||
@@ -289,7 +289,7 @@ describe('git', () => {
|
||||
it?
|
||||
</Fragment>,
|
||||
);
|
||||
await alertArgs.onConfirm();
|
||||
await alertArgs.onConfirm?.();
|
||||
};
|
||||
|
||||
it('should retry clone with .git suffix and succeed', async () => {
|
||||
|
||||
@@ -115,12 +115,43 @@ describe('workspace', () => {
|
||||
});
|
||||
|
||||
describe('activateWorkspace', () => {
|
||||
it('should do nothing if workspace cannot be found', async () => {
|
||||
const store = mockStore(await reduxStateForTest({ activeSpaceId: 'abc', activeWorkspaceId: 'def' }));
|
||||
|
||||
await store.dispatch(activateWorkspace({ workspaceId: 'DOES_NOT_EXIST' }));
|
||||
|
||||
expect(store.getActions()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should activate space and workspace and activity using workspaceId', async () => {
|
||||
const space = await models.space.create();
|
||||
const workspace = await models.workspace.create({ scope: 'design', parentId: space._id });
|
||||
const store = mockStore(await reduxStateForTest({ activeSpaceId: 'abc', activeWorkspaceId: 'def' }));
|
||||
|
||||
await store.dispatch(activateWorkspace({ workspaceId: workspace._id }));
|
||||
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
type: SET_ACTIVE_SPACE,
|
||||
spaceId: space._id,
|
||||
},
|
||||
{
|
||||
type: SET_ACTIVE_WORKSPACE,
|
||||
workspaceId: workspace._id,
|
||||
},
|
||||
{
|
||||
type: SET_ACTIVE_ACTIVITY,
|
||||
activity: ACTIVITY_SPEC,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should activate space and workspace and activity from home', async () => {
|
||||
const space = await models.space.create();
|
||||
const workspace = await models.workspace.create({ scope: 'design', parentId: space._id });
|
||||
const store = mockStore(await reduxStateForTest({ activeSpaceId: 'abc', activeWorkspaceId: 'def' }));
|
||||
|
||||
await store.dispatch(activateWorkspace(workspace));
|
||||
await store.dispatch(activateWorkspace({ workspace }));
|
||||
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
@@ -143,7 +174,7 @@ describe('workspace', () => {
|
||||
const workspace = await models.workspace.create({ scope: 'design', parentId: space._id });
|
||||
const store = mockStore(await reduxStateForTest({ activeSpaceId: space._id, activeWorkspaceId: workspace._id }));
|
||||
|
||||
await store.dispatch(activateWorkspace(workspace));
|
||||
await store.dispatch(activateWorkspace({ workspace }));
|
||||
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
@@ -166,7 +197,7 @@ describe('workspace', () => {
|
||||
const workspace = await models.workspace.create({ scope: 'design', parentId: space._id });
|
||||
const store = mockStore(await reduxStateForTest({ activeSpaceId: space._id, activeWorkspaceId: workspace._id, activeActivity }));
|
||||
|
||||
await store.dispatch(activateWorkspace(workspace));
|
||||
await store.dispatch(activateWorkspace({ workspace }));
|
||||
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
@@ -185,7 +216,7 @@ describe('workspace', () => {
|
||||
const workspace = await models.workspace.create({ scope: 'design', parentId: space._id });
|
||||
const store = mockStore(await reduxStateForTest({ activeSpaceId: space._id, activeWorkspaceId: workspace._id, activeActivity }));
|
||||
|
||||
await store.dispatch(activateWorkspace(workspace));
|
||||
await store.dispatch(activateWorkspace({ workspace }));
|
||||
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
@@ -204,7 +235,7 @@ describe('workspace', () => {
|
||||
const workspace = await models.workspace.create({ scope: 'collection', parentId: space._id });
|
||||
const store = mockStore(await reduxStateForTest({ activeSpaceId: space._id, activeWorkspaceId: workspace._id }));
|
||||
|
||||
await store.dispatch(activateWorkspace(workspace));
|
||||
await store.dispatch(activateWorkspace({ workspace }));
|
||||
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
@@ -229,7 +260,7 @@ describe('workspace', () => {
|
||||
await models.workspaceMeta.updateByParentId(workspace._id, { activeActivity: ACTIVITY_UNIT_TEST });
|
||||
const store = mockStore(await reduxStateForTest({ activeSpaceId: space._id, activeWorkspaceId: workspace._id }));
|
||||
|
||||
await store.dispatch(activateWorkspace(workspace));
|
||||
await store.dispatch(activateWorkspace({ workspace }));
|
||||
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Dispatch } from 'redux';
|
||||
import { RequireExactlyOne } from 'type-fest';
|
||||
|
||||
import { trackEvent, trackSegmentEvent } from '../../../common/analytics';
|
||||
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, GlobalActivity, isCollectionActivity, isDesignActivity } from '../../../common/constants';
|
||||
@@ -6,7 +7,7 @@ import { database } from '../../../common/database';
|
||||
import * as models from '../../../models';
|
||||
import { isCollection, isDesign, Workspace, WorkspaceScope } from '../../../models/workspace';
|
||||
import { showPrompt } from '../../components/modals';
|
||||
import { selectActiveActivity, selectActiveSpace } from '../selectors';
|
||||
import { selectActiveActivity, selectActiveSpace, selectAllWorkspaces } from '../selectors';
|
||||
import { RootState } from '.';
|
||||
import { setActiveActivity, setActiveSpace, setActiveWorkspace } from './global';
|
||||
|
||||
@@ -31,7 +32,7 @@ const actuallyCreate = (patch: Partial<Workspace>, onCreate?: OnWorkspaceCreateC
|
||||
}
|
||||
|
||||
trackEvent('Workspace', 'Create');
|
||||
await dispatch(activateWorkspace(workspace));
|
||||
await dispatch(activateWorkspace({ workspace }));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -71,8 +72,17 @@ export const createWorkspace = ({ scope, onCreate }: {
|
||||
};
|
||||
};
|
||||
|
||||
export const activateWorkspace = (workspace: Workspace) => {
|
||||
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 = selectAllWorkspaces(getState()).find(({ _id }) => _id === workspaceId);
|
||||
}
|
||||
|
||||
// If we still have no workspace, exit
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeActivity = selectActiveActivity(getState()) || undefined;
|
||||
|
||||
@@ -96,5 +106,7 @@ export const activateWorkspace = (workspace: Workspace) => {
|
||||
const nextActivity = cachedActivity as GlobalActivity || (isDesign(workspace) ? ACTIVITY_SPEC : ACTIVITY_DEBUG);
|
||||
dispatch(setActiveActivity(nextActivity));
|
||||
}
|
||||
|
||||
// TODO: dispatch one action to activate the space, workspace and activity in one go to avoid jumps in the UI
|
||||
};
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { BaseModel } from '../../models';
|
||||
import { getStatusCandidates } from '../../models/helpers/get-status-candidates';
|
||||
import { isRequest, Request } from '../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../models/request-group';
|
||||
import { BASE_SPACE_ID } from '../../models/space';
|
||||
import { BASE_SPACE_ID, isRemoteSpace } from '../../models/space';
|
||||
import { UnitTestResult } from '../../models/unit-test-result';
|
||||
import { RootState } from './modules';
|
||||
|
||||
@@ -76,6 +76,11 @@ export const selectSpaces = createSelector(
|
||||
entities => entities.spaces,
|
||||
);
|
||||
|
||||
export const selectRemoteSpaces = createSelector(
|
||||
selectSpaces,
|
||||
spaces => spaces.filter(isRemoteSpace),
|
||||
);
|
||||
|
||||
export const selectActiveSpace = createSelector(
|
||||
selectEntities,
|
||||
(state: RootState) => state.global.activeSpaceId,
|
||||
|
||||
Reference in new issue
Block a user