diff --git a/packages/insomnia/src/sync/vcs/migrate-projects-into-organization.ts b/packages/insomnia/src/sync/vcs/migrate-projects-into-organization.ts new file mode 100644 index 000000000..8d3101ba1 --- /dev/null +++ b/packages/insomnia/src/sync/vcs/migrate-projects-into-organization.ts @@ -0,0 +1,124 @@ +import { getAccountId, getCurrentSessionId } from '../../account/session'; +import { database } from '../../common/database'; +import * as models from '../../models'; +import { isOwnerOfOrganization, isPersonalOrganization } from '../../models/organization'; +import { Project, RemoteProject } from '../../models/project'; +import { OrganizationsResponse } from '../../ui/routes/organization'; +import { invariant } from '../../utils/invariant'; + +let status: 'idle' | 'pending' | 'error' | 'completed' = 'idle'; + +// TODO: +// Error handling and return type for errors + +// Migration: +// Team ~= Project > Workspaces +// In the previous API: { _id: 'proj_team_123', remoteId: 'team_123', parentId: null } + +// Organization > TeamProject > Workspaces +// In the new API: { _id: 'proj_team_123', remoteId: 'proj_team_123', parentId: 'team_123' } + +export const shouldMigrateProjectUnderOrganization = async () => { + const localProjectCount = await database.count(models.project.type, { + remoteId: null, + parentId: null, + _id: { $ne: models.project.SCRATCHPAD_PROJECT_ID }, + }); + + const legacyRemoteProjectCount = await database.count(models.project.type, { + remoteId: { $ne: null }, + parentId: null, + }); + + return localProjectCount > 0 || legacyRemoteProjectCount > 0; +}; + +export const migrateProjectsIntoOrganization = async () => { + if (status !== 'idle' && status !== 'error') { + return; + } + + status = 'pending'; + + try { + const sessionId = getCurrentSessionId(); + invariant(sessionId, 'User must be logged in to migrate projects'); + + // local projects what if they dont have a parentId? + // after migration: all projects have a parentId + + // no more hostage projects + // when will we know what org is you have? + // already migrated: all things are globes + // already migrated with a blank account what happens to previous account data? + // go to whatever + + // about to migrate: have one or more projects without parentIds/null + // if the project doesn't have remoteId we can throw in the home org + // if the project has a remoteId, and is in the org, we know it should have org as a parentId + // if the project has a remoteId, and is not in my logged in org + // go to the whatever + + // whatever + // export all + // 1. show a alert describing the state of the orphaned projects and instructing export all and reimport + // 2. show a recovery ux to move old workspaces into existing projects + // 3. show orphaned projects in the home organization + // 4. show disabled orgs in the sidebar from previous account where you can see the data + + // todo + // 1. [x] only assign parentIds and migrate old remote logic + // 2. count orphaned projects + // 3. decide which approach take for orphaned projects + // 4. decide if theres no reason to keep migrateCollectionsIntoRemoteProject + + // assign remote project parentIds to new organization structure + // the remote id field used to track team_id (remote concept for matching 1:1 with this project) which is now org_id + // the _id field used to track the proj_team_id which was a wrapper for the team_id prefixing proj_to the above id, + // which is now the remoteId for tracking the projects within an org + const legacyRemoteProjects = await database.find(models.project.type, { + remoteId: { $ne: null }, + parentId: null, + }); + for (const remoteProject of legacyRemoteProjects) { + await models.project.update(remoteProject, { + parentId: remoteProject.remoteId, + remoteId: remoteProject._id, + }); + } + + // Local projects without organizations except scratchpad + const localProjects = await database.find(models.project.type, { + remoteId: null, + parentId: null, + _id: { $ne: models.project.SCRATCHPAD_PROJECT_ID }, + }); + const organizationsResult = await window.main.insomniaFetch({ + method: 'GET', + path: '/v1/organizations', + sessionId, + }); + const accountId = getAccountId(); + invariant(organizationsResult, 'Failed to fetch organizations'); + invariant(accountId, 'Failed to get account id'); + const { organizations } = organizationsResult; + const personalOrganization = organizations.filter(isPersonalOrganization) + .find(organization => + isOwnerOfOrganization({ + organization, + accountId, + })); + invariant(personalOrganization, 'Failed to find personal organization'); + + for (const localProject of localProjects) { + await models.project.update(localProject, { + parentId: personalOrganization.id, + }); + } + + status = 'completed'; + } catch (err) { + console.warn('Failed to migrate projects to personal workspace', err); + throw err; + } +}; diff --git a/packages/insomnia/src/sync/vcs/migrate-to-cloud-projects.ts b/packages/insomnia/src/sync/vcs/migrate-to-cloud-projects.ts deleted file mode 100644 index 48eef74b1..000000000 --- a/packages/insomnia/src/sync/vcs/migrate-to-cloud-projects.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { getCurrentSessionId } from '../../account/session'; -import { database } from '../../common/database'; -import * as models from '../../models'; -import { Project, RemoteProject } from '../../models/project'; -import { Workspace } from '../../models/workspace'; -import { invariant } from '../../utils/invariant'; -import { initializeLocalBackendProjectAndMarkForSync, pushSnapshotOnInitialize } from './initialize-backend-project'; -import { VCS } from './vcs'; - -let status: 'idle' | 'pending' | 'error' | 'completed' = 'idle'; - -// TODO: -// Error handling and return type for errors - -// Migration: -// Team ~= Project > Workspaces -// In the previous API: { _id: 'proj_team_123', remoteId: 'team_123', parentId: null } - -// Organization > TeamProject > Workspaces -// In the new API: { _id: 'proj_team_123', remoteId: 'proj_team_123', parentId: 'team_123' } - -export const shouldRunMigration = async () => { - const localProjects = await database.find(models.project.type, { - remoteId: null, - _id: { $ne: models.project.SCRATCHPAD_PROJECT_ID }, - }); - - const legacyRemoteProjects = await database.find(models.project.type, { - remoteId: { $ne: null }, - parentId: null, - }); - - return localProjects.length > 0 || legacyRemoteProjects.length > 0; -}; - -// Second time we run this, whats gonna happen? -// we will get duplicate projects in the cloud and local with the same but no workspaces in the duplicate - -export const migrateLocalToCloudProjects = async (vcs: VCS) => { - if (status !== 'idle' && status !== 'error') { - return; - } - - status = 'pending'; - - try { - const sessionId = getCurrentSessionId(); - invariant(sessionId, 'User must be logged in to migrate projects'); - - // Migrate legacy remote projects to new organization structure - const legacyRemoteProjects = await database.find(models.project.type, { - remoteId: { $ne: null }, - parentId: null, - }); - - for (const remoteProject of legacyRemoteProjects) { - await models.project.update(remoteProject, { - // Remote Id was previously the teamId - parentId: remoteProject.remoteId, - // _id was previously the remoteId - remoteId: remoteProject._id, - }); - } - - // Local projects except scratchpad - const localProjects = await database.find(models.project.type, { - remoteId: null, - _id: { $ne: models.project.SCRATCHPAD_PROJECT_ID }, - }); - for (const localProject of localProjects) { - // -- Unsafe to run twice, will cause duplicates unless we would need to match ids - const newCloudProject = await window.main.insomniaFetch<{ id: string; name: string; organizationId: string } | void>({ - path: '/v1/organizations/personal/team-projects', - method: 'POST', - data: { - name: localProject.name, - }, - sessionId, - }); - - invariant(typeof newCloudProject?.id === 'string', 'Failed to create remote project'); - - const project = await models.project.update(localProject, { - name: newCloudProject.name, - remoteId: newCloudProject.id, - parentId: newCloudProject.organizationId, - }); - - // For each workspace in the local project - const projectWorkspaces = (await database.find(models.workspace.type, { - parentId: localProject._id, - })); - - for (const workspace of projectWorkspaces) { - const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); - - // Initialize Sync on the workspace if it's not using Git sync - try { - if (!workspaceMeta.gitRepositoryId) { - invariant(vcs, 'VCS must be initialized'); - - await initializeLocalBackendProjectAndMarkForSync({ vcs, workspace }); - await pushSnapshotOnInitialize({ vcs, workspace, project }); - } - } catch (e) { - console.warn('Failed to initialize sync on workspace. This will be retried when the workspace is opened on the app.', e); - // TODO: here we should show the try again dialog - } - } - } - - status = 'completed'; - } catch (err) { - console.warn('Failed to migrate projects to cloud', err); - throw err; - } -}; diff --git a/packages/insomnia/src/ui/components/settings/import-export.tsx b/packages/insomnia/src/ui/components/settings/import-export.tsx index b19e4dba8..150f28221 100644 --- a/packages/insomnia/src/ui/components/settings/import-export.tsx +++ b/packages/insomnia/src/ui/components/settings/import-export.tsx @@ -1,27 +1,27 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; +import { Button, Heading, Item, ListBox, Popover, Select, SelectValue } from 'react-aria-components'; import { useFetcher, useParams } from 'react-router-dom'; import { useRouteLoaderData } from 'react-router-dom'; import { isLoggedIn } from '../../../account/session'; import { getProductName } from '../../../common/constants'; -import { docsImportExport } from '../../../common/documentation'; import { exportAllToFile } from '../../../common/export'; import { exportAllData } from '../../../common/export-all-data'; import { getWorkspaceLabel } from '../../../common/get-workspace-label'; import { strings } from '../../../common/strings'; -import { isScratchpadOrganizationId } from '../../../models/organization'; +import { isScratchpadOrganizationId, Organization } from '../../../models/organization'; import { isScratchpad } from '../../../models/workspace'; import { SegmentEvent } from '../../analytics'; +import { useOrganizationLoaderData } from '../../routes/organization'; import { ProjectLoaderData } from '../../routes/project'; import { useRootLoaderData } from '../../routes/root'; +import { LoaderData } from '../../routes/untracked-projects'; import { WorkspaceLoaderData } from '../../routes/workspace'; -import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; -import { Link } from '../base/link'; +import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; import { Icon } from '../icon'; import { showAlert } from '../modals'; import { ExportRequestsModal } from '../modals/export-requests-modal'; import { ImportModal } from '../modals/import-modal'; -import { Button } from '../themed-button'; interface Props { hideSettingsModal: () => void; } @@ -32,6 +32,19 @@ export const ImportExport: FC = ({ hideSettingsModal }) => { projectId, workspaceId, } = useParams() as { organizationId: string; projectId: string; workspaceId?: string }; + const { organizations } = useOrganizationLoaderData(); + + const untrackedProjectsFetcher = useFetcher(); + const moveProjectFetcher = useFetcher(); + + useEffect(() => { + const isIdleAndUninitialized = untrackedProjectsFetcher.state === 'idle' && !untrackedProjectsFetcher.data; + if (isIdleAndUninitialized) { + untrackedProjectsFetcher.load('/untracked-projects'); + } + }, [untrackedProjectsFetcher, organizationId]); + + const untrackedProjects = untrackedProjectsFetcher.data?.untrackedProjects || []; const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined; const activeWorkspaceName = workspaceData?.activeWorkspace.name; @@ -57,123 +70,214 @@ export const ImportExport: FC = ({ hideSettingsModal }) => { if (!organizationId) { return null; } - // here we should list all the folders which contain insomnia.*.db files - // and have some big red button to overwrite the current data with the backup - // and once complete trigger an app restart? + return ( -
-
- Import format will be automatically detected. -
-

- Your format isn't supported? Add Your Own. -

-
- {workspaceData?.activeWorkspace ? - isScratchpad(workspaceData.activeWorkspace) ? - - : - ( - Export Data - - } - > - - - setIsExportModalOpen(true)} - /> - - - - - - ) : () - } - + : + ( + + Export Data + + } + > + + + setIsExportModalOpen(true)} + /> + + + + + + + ) : ( + + ) + } + - showAlert({ - title: 'Export Complete', - message: 'All your data have been successfully exported', - }); - window.main.trackSegmentEvent({ - event: SegmentEvent.exportAllCollections, - }); - }} - aria-label='Export all data' - className="px-4 py-1 font-semibold border border-solid border-[--hl-md] flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-base" - > - - Export all data {`(${workspaceCount} files)`} - - - - + +
+
+ Import: +
+ +
+
+ {untrackedProjects.length > 0 &&
+
+ Untracked projects ({untrackedProjects.length}) +

+ These projects are not associated with any organization in your account. You can move them to an organization below. +

+
+
+ {untrackedProjects.map(project => ( +
+
+ + {project.name} + + Id: {project._id} + + +

+ This project contains {project.workspacesCount} {project.workspacesCount === 1 ? 'file' : 'files'}. +

+
+
{ + e.preventDefault(); + moveProjectFetcher.submit(e.currentTarget, { + action: `/organization/${organizationId}/project/${project._id}/move`, + method: 'POST', + }); + }} + > + + +
+
+ ))} +
+
} {isImportModalOpen && ( { useDocBodyKeyboardShortcuts({ plugin_reload: () => plugins.reloadPlugins(), + // TODO: move this to workspace route environment_showVariableSourceAndValue: () => patchSettings({ showVariableSourceAndValue: !settings.showVariableSourceAndValue }), + // TODO: move this to organization route preferences_showGeneral: () => showModal(SettingsModal), preferences_showKeyboardShortcuts: () => showModal(SettingsModal, { tab: TAB_INDEX_SHORTCUTS }), + // TODO: move this to workspace route sidebar_toggle: () => activeWorkspaceMeta && patchWorkspaceMeta(activeWorkspaceMeta.parentId, { sidebarHidden: !activeWorkspaceMeta.sidebarHidden }), }); diff --git a/packages/insomnia/src/ui/index.tsx b/packages/insomnia/src/ui/index.tsx index 3c3e17acd..442155eb6 100644 --- a/packages/insomnia/src/ui/index.tsx +++ b/packages/insomnia/src/ui/index.tsx @@ -151,6 +151,10 @@ const router = createMemoryRouter( action: async (...args) => (await import('./routes/actions')).updateSettingsAction(...args), }, + { + path: 'untracked-projects', + loader: async (...args) => (await import('./routes/untracked-projects')).loader(...args), + }, { path: 'organization', id: '/organization', @@ -209,6 +213,13 @@ const router = createMemoryRouter( await import('./routes/actions') ).deleteProjectAction(...args), }, + { + path: 'move', + action: async (...args) => + ( + await import('./routes/actions') + ).moveProjectAction(...args), + }, { path: 'rename', action: async (...args) => diff --git a/packages/insomnia/src/ui/routes/actions.tsx b/packages/insomnia/src/ui/routes/actions.tsx index 8dee1907f..50012e626 100644 --- a/packages/insomnia/src/ui/routes/actions.tsx +++ b/packages/insomnia/src/ui/routes/actions.tsx @@ -28,12 +28,23 @@ export const createNewProjectAction: ActionFunction = async ({ request, params } const { organizationId } = params; invariant(organizationId, 'Organization ID is required'); const formData = await request.formData(); - const name = formData.get('name'); + const name = formData.get('name') || 'My project'; invariant(typeof name === 'string', 'Name is required'); + const projectType = formData.get('type'); + invariant(projectType === 'local' || projectType === 'remote', 'Project type is required'); const sessionId = session.getCurrentSessionId(); invariant(sessionId, 'User must be logged in to create a project'); + if (projectType === 'local') { + const project = await models.project.create({ + name, + parentId: organizationId, + }); + + return redirect(`/organization/${organizationId}/project/${project._id}`); + } + try { const newCloudProject = await window.main.insomniaFetch<{ id: string; @@ -97,22 +108,24 @@ export const renameProjectAction: ActionFunction = async ({ invariant(sessionId, 'User must be logged in to rename a project'); try { - const response = await window.main.insomniaFetch({ - path: `/v1/organizations/${project.parentId}/team-projects/${project.remoteId}`, - method: 'PATCH', - sessionId, - data: { - name, - }, - }); + if (project.remoteId) { + const response = await window.main.insomniaFetch({ + path: `/v1/organizations/${project.parentId}/team-projects/${project.remoteId}`, + method: 'PATCH', + sessionId, + data: { + name, + }, + }); - if (response && 'error' in response) { - return { - error: response.error === 'FORBIDDEN' ? 'You do not have permission to rename this project.' : 'An unexpected error occurred while renaming the project. Please try again.', - }; + if (response && 'error' in response) { + return { + error: response.error === 'FORBIDDEN' ? 'You do not have permission to rename this project.' : 'An unexpected error occurred while renaming the project. Please try again.', + }; + } } await models.project.update(project, { name }); @@ -136,19 +149,21 @@ export const deleteProjectAction: ActionFunction = async ({ params }) => { invariant(sessionId, 'User must be logged in to delete a project'); try { - const response = await window.main.insomniaFetch({ - path: `/v1/organizations/${organizationId}/team-projects/${project.remoteId}`, - method: 'DELETE', - sessionId, - }); + if (project.remoteId) { + const response = await window.main.insomniaFetch({ + path: `/v1/organizations/${organizationId}/team-projects/${project.remoteId}`, + method: 'DELETE', + sessionId, + }); - if (response && 'error' in response) { - return { - error: response.error === 'FORBIDDEN' ? 'You do not have permission to delete this project.' : 'An unexpected error occurred while deleting the project. Please try again.', - }; + if (response && 'error' in response) { + return { + error: response.error === 'FORBIDDEN' ? 'You do not have permission to delete this project.' : 'An unexpected error occurred while deleting the project. Please try again.', + }; + } } await models.stats.incrementDeletedRequestsForDescendents(project); @@ -163,6 +178,27 @@ export const deleteProjectAction: ActionFunction = async ({ params }) => { } }; +export const moveProjectAction: ActionFunction = async ({ request, params }) => { + const { projectId } = params as { projectId: string }; + const formData = await request.formData(); + + const organizationId = formData.get('organizationId'); + + invariant(typeof organizationId === 'string', 'Organization ID is required'); + invariant(typeof projectId === 'string', 'Project ID is required'); + + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + + await models.project.update(project, { + parentId: organizationId, + // We move a project to another organization as local no matter what it was before + remoteId: null, + }); + + return null; +}; + // Workspace export const createNewWorkspaceAction: ActionFunction = async ({ params, diff --git a/packages/insomnia/src/ui/routes/auth.authorize.tsx b/packages/insomnia/src/ui/routes/auth.authorize.tsx index ab731eedf..fb1f2818c 100644 --- a/packages/insomnia/src/ui/routes/auth.authorize.tsx +++ b/packages/insomnia/src/ui/routes/auth.authorize.tsx @@ -3,7 +3,7 @@ import { Heading } from 'react-aria-components'; import { ActionFunction, redirect, useFetcher, useFetchers, useNavigate } from 'react-router-dom'; import { isLoggedIn } from '../../account/session'; -import { shouldRunMigration } from '../../sync/vcs/migrate-to-cloud-projects'; +import { shouldMigrateProjectUnderOrganization } from '../../sync/vcs/migrate-projects-into-organization'; import { invariant } from '../../utils/invariant'; import { getLoginUrl, submitAuthCode } from '../auth-session-provider'; import { Icon } from '../components/icon'; @@ -26,7 +26,7 @@ export const action: ActionFunction = async ({ console.log('Login successful'); window.localStorage.setItem('hasUserLoggedInBefore', 'true'); - if (isLoggedIn() && await shouldRunMigration()) { + if (isLoggedIn() && await shouldMigrateProjectUnderOrganization()) { throw redirect('/auth/migrate'); } diff --git a/packages/insomnia/src/ui/routes/auth.login.tsx b/packages/insomnia/src/ui/routes/auth.login.tsx index 0b3931f55..c33c23bd7 100644 --- a/packages/insomnia/src/ui/routes/auth.login.tsx +++ b/packages/insomnia/src/ui/routes/auth.login.tsx @@ -4,7 +4,7 @@ import { ActionFunction, Link, LoaderFunction, redirect, useFetcher, useLoaderDa import { getAppWebsiteBaseURL } from '../../common/constants'; import { exportAllData } from '../../common/export-all-data'; -import { shouldRunMigration } from '../../sync/vcs/migrate-to-cloud-projects'; +import { shouldMigrateProjectUnderOrganization } from '../../sync/vcs/migrate-projects-into-organization'; import { SegmentEvent } from '../analytics'; import { getLoginUrl } from '../auth-session-provider'; import { Icon } from '../components/icon'; @@ -39,7 +39,7 @@ interface LoaderData { } export const loader: LoaderFunction = async () => { - const hasProjectsToMigrate = await shouldRunMigration(); + const hasProjectsToMigrate = await shouldMigrateProjectUnderOrganization(); return { hasProjectsToMigrate, diff --git a/packages/insomnia/src/ui/routes/auth.migrate.tsx b/packages/insomnia/src/ui/routes/auth.migrate.tsx index fe9954f1b..5ebb88aaf 100644 --- a/packages/insomnia/src/ui/routes/auth.migrate.tsx +++ b/packages/insomnia/src/ui/routes/auth.migrate.tsx @@ -5,12 +5,12 @@ import { ActionFunction, LoaderFunction, redirect, useFetcher } from 'react-rout import { getCurrentSessionId, logout } from '../../account/session'; import FileSystemDriver from '../../sync/store/drivers/file-system-driver'; import { migrateCollectionsIntoRemoteProject } from '../../sync/vcs/migrate-collections'; -import { migrateLocalToCloudProjects, shouldRunMigration } from '../../sync/vcs/migrate-to-cloud-projects'; +import { migrateProjectsIntoOrganization, shouldMigrateProjectUnderOrganization } from '../../sync/vcs/migrate-projects-into-organization'; import { VCS } from '../../sync/vcs/vcs'; import { Icon } from '../components/icon'; export const loader: LoaderFunction = async () => { - if (!shouldRunMigration()) { + if (!shouldMigrateProjectUnderOrganization()) { return redirect('/organization'); } @@ -32,7 +32,7 @@ export const action: ActionFunction = async () => { const driver = FileSystemDriver.create(process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData')); const vcs = new VCS(driver); await migrateCollectionsIntoRemoteProject(vcs); - await migrateLocalToCloudProjects(vcs); + await migrateProjectsIntoOrganization(); return redirect('/organization'); } catch (err) { @@ -61,13 +61,13 @@ export const Migrate = () => { return (
- Migrating data to Insomnia Cloud + Initializing Organizations {isMigrating && (
- Running migration... + Fetching your organizations...
)} diff --git a/packages/insomnia/src/ui/routes/onboarding.cloud-migration.tsx b/packages/insomnia/src/ui/routes/onboarding.cloud-migration.tsx index af4cd5868..8c757d547 100644 --- a/packages/insomnia/src/ui/routes/onboarding.cloud-migration.tsx +++ b/packages/insomnia/src/ui/routes/onboarding.cloud-migration.tsx @@ -4,7 +4,7 @@ import { ActionFunction, LoaderFunction, redirect, useFetcher } from 'react-rout import { logout } from '../../account/session'; import { exportAllData } from '../../common/export-all-data'; -import { shouldRunMigration } from '../../sync/vcs/migrate-to-cloud-projects'; +import { shouldMigrateProjectUnderOrganization } from '../../sync/vcs/migrate-projects-into-organization'; import { SegmentEvent } from '../analytics'; import { InsomniaLogo } from '../components/insomnia-icon'; import { showAlert } from '../components/modals'; @@ -17,7 +17,7 @@ export const action: ActionFunction = async () => { }; export const loader: LoaderFunction = async () => { - if (await shouldRunMigration()) { + if (await shouldMigrateProjectUnderOrganization()) { return null; } diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index be951d721..ad0693a8a 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -35,7 +35,7 @@ import { isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganization import { isDesign, isScratchpad } from '../../models/workspace'; import FileSystemDriver from '../../sync/store/drivers/file-system-driver'; import { MergeConflict } from '../../sync/types'; -import { shouldRunMigration } from '../../sync/vcs/migrate-to-cloud-projects'; +import { shouldMigrateProjectUnderOrganization } from '../../sync/vcs/migrate-projects-into-organization'; import { getVCS, initVCS } from '../../sync/vcs/vcs'; import { invariant } from '../../utils/invariant'; import { SegmentEvent } from '../analytics'; @@ -55,7 +55,7 @@ import { PresenceProvider } from '../context/app/presence-context'; import { useRootLoaderData } from './root'; import { WorkspaceLoaderData } from './workspace'; -interface OrganizationsResponse { +export interface OrganizationsResponse { start: number; limit: number; length: number; @@ -90,7 +90,7 @@ interface CurrentPlan { type: PersonalPlanType; }; -const organizationsData: OrganizationLoaderData = { +export const organizationsData: OrganizationLoaderData = { organizations: [], user: undefined, currentPlan: undefined, @@ -121,7 +121,7 @@ export const indexLoader: LoaderFunction = async () => { if (sessionId) { // Check if there are any migrations to run before loading organizations. // If there are migrations, we need to log the user out and redirect them to the login page - if (await shouldRunMigration()) { + if (await shouldMigrateProjectUnderOrganization()) { await session.logout(); return redirect('/auth/login'); } @@ -175,13 +175,12 @@ export const indexLoader: LoaderFunction = async () => { organizationsData.user = user; organizationsData.currentPlan = currentPlan; - const personalOrganization = organizations.filter(isPersonalOrganization).find(organization => { - const accountId = getAccountId(); - return accountId && isOwnerOfOrganization({ - organization, - accountId, - }); - }); + const personalOrganization = organizations.filter(isPersonalOrganization) + .find(organization => + isOwnerOfOrganization({ + organization, + accountId, + })); if (personalOrganization) { return redirect(`/organization/${personalOrganization.id}`); diff --git a/packages/insomnia/src/ui/routes/project.tsx b/packages/insomnia/src/ui/routes/project.tsx index 97f8839b5..c9a2c28a4 100644 --- a/packages/insomnia/src/ui/routes/project.tsx +++ b/packages/insomnia/src/ui/routes/project.tsx @@ -2,17 +2,25 @@ import { IconName } from '@fortawesome/fontawesome-svg-core'; import React, { FC, Fragment, useEffect, useState } from 'react'; import { Button, + Dialog, + DialogTrigger, GridList, Heading, Input, Item, + Label, ListBox, Menu, MenuTrigger, + Modal, + ModalOverlay, Popover, + Radio, + RadioGroup, SearchField, Select, SelectValue, + TextField, } from 'react-aria-components'; import { LoaderFunction, @@ -37,7 +45,6 @@ import { import { database } from '../../common/database'; import { fuzzyMatchAll, isNotNullOrUndefined } from '../../common/misc'; import { descendingNumberSort, sortMethodMap } from '../../common/sorting'; -import { strings } from '../../common/strings'; import * as models from '../../models'; import { ApiSpec } from '../../models/api-spec'; import { CaCertificate } from '../../models/ca-certificate'; @@ -161,18 +168,6 @@ async function syncTeamProjects({ }); } })); - - // Remove any remote projects from the current organization that are not in the list of remote projects - const removedRemoteProjects = await database.find(models.project.type, { - // filter by this organization so no legacy data can be accidentally removed, because legacy had null parentId - parentId: organizationId, - // Remote ID is not in the list of remote projects - remoteId: { $nin: teamProjects.map(p => p.id) }, - }); - - await Promise.all(removedRemoteProjects.map(async prj => { - await models.project.remove(prj); - })); } export const indexLoader: LoaderFunction = async ({ params }) => { @@ -750,33 +745,94 @@ const ProjectRoute: FC = () => {
+ + + + + + {({ close }) => ( +
+
+ Create a new project + +
+
{ + createNewProjectFetcher.submit(e.currentTarget, { + action: `/organization/${organizationId}/project/new`, + method: 'post', + }); - + close(); + }} + > + + + + + + +
+ + + Secure Cloud +

+ End-to-end encrypted (E2EE) and synced securely to the cloud, ideal for collaboration. +

+
+ + + Local Vault +

+ Stored locally only with no cloud. Ideal when collaboration is not needed. +

+
+
+
+
+ +
+
+
+ )} +
+
+
+
{ + const { organizations } = organizationsData; + const listOfOrganizationIds = [...organizations.map(o => o.id), SCRATCHPAD_ORGANIZATION_ID]; + + const projects = await database.find('Project', { + parentId: { $nin: listOfOrganizationIds }, + }); + + const untrackedProjects = []; + + for (const project of projects) { + const workspacesCount = await database.count('Workspace', { + parentId: project._id, + }); + + untrackedProjects.push({ + ...project, + workspacesCount, + }); + } + + return { + untrackedProjects, + }; +};