diff --git a/packages/insomnia/src/models/organization.ts b/packages/insomnia/src/models/organization.ts index 80a38f07538..991521fc18b 100644 --- a/packages/insomnia/src/models/organization.ts +++ b/packages/insomnia/src/models/organization.ts @@ -28,3 +28,12 @@ export const isOwnerOfOrganization = ({ accountId: string; }) => organization.metadata.ownerAccountId === accountId; + +export const findPersonalOrganization = (organizations: Organization[], accountId: string) => { + return organizations.filter(isPersonalOrganization) + .find(organization => + isOwnerOfOrganization({ + organization, + accountId, + })); +}; diff --git a/packages/insomnia/src/sync/vcs/migrate-projects-into-organization.ts b/packages/insomnia/src/sync/vcs/migrate-projects-into-organization.ts index 3aa00729589..34ac9394607 100644 --- a/packages/insomnia/src/sync/vcs/migrate-projects-into-organization.ts +++ b/packages/insomnia/src/sync/vcs/migrate-projects-into-organization.ts @@ -1,6 +1,5 @@ import { database } from '../../common/database'; import * as models from '../../models'; -import { Organization } from '../../models/organization'; import { Project, RemoteProject } from '../../models/project'; // Migration: @@ -32,9 +31,9 @@ export const shouldMigrateProjectUnderOrganization = async () => { }; export const migrateProjectsIntoOrganization = async ({ - personalOrganization, + personalOrganizationId, }: { - personalOrganization: Organization; + personalOrganizationId: string; }) => { // Legacy remote projects without organizations // Local projects without organizations except scratchpad @@ -65,7 +64,7 @@ export const migrateProjectsIntoOrganization = async ({ for (const localProject of localProjects) { updatePromises.push( models.project.update(localProject, { - parentId: personalOrganization.id, + parentId: personalOrganizationId, }) ); } diff --git a/packages/insomnia/src/ui/hooks/use-loader-defer-data.ts b/packages/insomnia/src/ui/hooks/use-loader-defer-data.ts new file mode 100644 index 00000000000..4a0623ee4f5 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/use-loader-defer-data.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; + +export const useLoaderDeferData = (deferedDataPromise?: Promise): [T | undefined, boolean, any] => { + const [data, setData] = useState(); + const [error, setError] = useState(); + const [isPending, setIsPending] = useState(true); + useEffect(() => { + if (deferedDataPromise === undefined) { + return; + } + (async () => { + try { + const data = await deferedDataPromise; + setIsPending(false); + setData(data); + } catch (err) { + setError(err); + console.error('Failed to load defered data', err); + } + })(); + }, [deferedDataPromise]); + + return [data, isPending, error]; +}; diff --git a/packages/insomnia/src/ui/hooks/use-organization-async-task.ts b/packages/insomnia/src/ui/hooks/use-organization-async-task.ts new file mode 100644 index 00000000000..e0467719fa4 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/use-organization-async-task.ts @@ -0,0 +1,58 @@ +import { useEffect, useMemo } from 'react'; +import { useFetcher, useLocation } from 'react-router-dom'; + +import { findPersonalOrganization, Organization } from '../../models/organization'; +import { UserSession } from '../../models/user-session'; +import { AsyncTask } from '../routes/organization'; + +interface OrganizationSync { + organizationId: string; + organizations: Organization[]; + userSession: UserSession; +} + +// this hook is used to run async task for organizations and projects (such as sync remote or migration) +// return status so that we can show loading or error message to user +export const useAsyncTask = ({ + organizationId, + organizations, + userSession, +}: OrganizationSync) => { + const asyncTaskFetcher = useFetcher(); + const location = useLocation(); + console.log('location', location); + + const asyncTaskStatus = useMemo(() => { + const data = asyncTaskFetcher.data; + console.log('fetcherdata', data, asyncTaskFetcher.state); + if (asyncTaskFetcher.state !== 'idle' && data?.error) { + return 'error'; + } + return asyncTaskFetcher.state; + }, [asyncTaskFetcher.state, asyncTaskFetcher.data]); + + const personalOrganization = findPersonalOrganization(organizations, userSession.accountId); + + const asyncTaskList = location.state?.asyncTaskList as AsyncTask[]; + + useEffect(() => { + console.log('asyncTaskList', asyncTaskList); + if (asyncTaskList?.length) { + console.log('run async task in useEffect'); + const submit = asyncTaskFetcher.submit; + submit({ + sessionId: userSession.id, + accountId: userSession.accountId, + personalOrganizationId: personalOrganization?.id || '', + organizationId, + asyncTaskList: asyncTaskList, + }, { + action: '/organization/asyncTask', + method: 'POST', + encType: 'application/json', + }); + } + }, [asyncTaskList, asyncTaskFetcher.submit, userSession.id, personalOrganization?.id, organizationId, userSession.accountId]); + + return asyncTaskStatus; +}; diff --git a/packages/insomnia/src/ui/index.tsx b/packages/insomnia/src/ui/index.tsx index 6c756efef39..c10542bec41 100644 --- a/packages/insomnia/src/ui/index.tsx +++ b/packages/insomnia/src/ui/index.tsx @@ -25,6 +25,7 @@ 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 { getInitialEntry } from '../utils/router'; import { AppLoadingIndicator } from './components/app-loading-indicator'; import Auth from './routes/auth'; import Authorize from './routes/auth.authorize'; @@ -61,34 +62,6 @@ try { console.log('Failed to parse session data', e); } -async function getInitialEntry() { - // If the user has not seen the onboarding, then show it - // Otherwise if the user is not logged in and has not logged in before, then show the login - // Otherwise if the user is logged in, then show the organization - try { - const hasSeenOnboardingV9 = Boolean(window.localStorage.getItem('hasSeenOnboardingV9')); - - if (!hasSeenOnboardingV9) { - return '/onboarding'; - } - - const hasUserLoggedInBefore = window.localStorage.getItem('hasUserLoggedInBefore'); - - const user = await models.userSession.getOrCreate(); - if (user.id) { - return '/organization'; - } - - if (hasUserLoggedInBefore) { - return '/auth/login'; - } - - return '/organization/org_scratchpad/project/proj_scratchpad/workspace/wrk_scratchpad/debug'; - } catch (e) { - return '/organization/org_scratchpad/project/proj_scratchpad/workspace/wrk_scratchpad/debug'; - } -} - async function renderApp() { await database.initClient(); await initPlugins(); @@ -201,6 +174,10 @@ async function renderApp() { path: 'sync', action: async (...args) => (await import('./routes/organization')).syncOrganizationsAction(...args), }, + { + path: 'asyncTask', + action: async (...args) => (await import('./routes/organization')).asyncTaskAction(...args), + }, { path: ':organizationId', id: ':organizationId', diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index 13c2f072368..bb9f44437d7 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -30,7 +30,7 @@ import { getAppWebsiteBaseURL } from '../../common/constants'; import { database } from '../../common/database'; import { userSession } from '../../models'; import { updateLocalProjectToRemote } from '../../models/helpers/project'; -import { isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganizationId, Organization } from '../../models/organization'; +import { findPersonalOrganization, isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganizationId, Organization } from '../../models/organization'; import { Project } from '../../models/project'; import { isDesign, isScratchpad } from '../../models/workspace'; import { VCSInstance } from '../../sync/vcs/insomnia-sync'; @@ -53,6 +53,8 @@ import { PresentUsers } from '../components/present-users'; import { Toast } from '../components/toast'; import { useAIContext } from '../context/app/ai-context'; import { InsomniaEventStreamProvider } from '../context/app/insomnia-event-stream-context'; +import { useAsyncTask } from '../hooks/use-organization-async-task'; +import { syncProjects } from './project'; import { useRootLoaderData } from './root'; import { UntrackedProjectsLoaderData } from './untracked-projects'; import { WorkspaceLoaderData } from './workspace'; @@ -108,6 +110,12 @@ interface CurrentPlan { type: PersonalPlanType; }; +export const enum AsyncTask { + SyncOrganization, + MigrateProjects, + SyncProjects, +} + function sortOrganizations(accountId: string, organizations: Organization[]): Organization[] { const home = organizations.find(organization => isPersonalOrganization(organization) && isOwnerOfOrganization({ organization, @@ -164,7 +172,79 @@ async function syncOrganization(sessionId: string, accountId: string) { } } +interface AsyncTaskActionRequest { + sessionId: string; + accountId: string; + personalOrganizationId: string; + organizationId: string; + asyncTaskList: AsyncTask[]; +} + +// this action is used to run task that we dont want to block the UI +export const asyncTaskAction: ActionFunction = async ({ request }) => { + try { + const { sessionId, personalOrganizationId, organizationId, asyncTaskList, accountId } = await request.json() as AsyncTaskActionRequest; + + const taskPromiseList = []; + + for (const task of asyncTaskList) { + if (task === AsyncTask.SyncOrganization) { + invariant(sessionId, 'sessionId is required'); + invariant(accountId, 'accountId is required'); + taskPromiseList.push(syncOrganization(sessionId, accountId)); + } + + if (task === AsyncTask.MigrateProjects) { + invariant(personalOrganizationId, 'personalOrganizationId is required'); + invariant(sessionId, 'sessionId is required'); + taskPromiseList.push(migrateProjectsUnderOrganization(personalOrganizationId, sessionId)); + } + + if (task === AsyncTask.SyncProjects) { + invariant(organizationId, 'organizationId is required'); + taskPromiseList.push(syncProjects(organizationId)); + } + } + + await Promise.all(taskPromiseList); + + return {}; + } catch (error) { + console.log('Failed to run async task', error); + return { + error: error.message, + }; + } +}; + +async function migrateProjectsUnderOrganization(personalOrganizationId: string, sessionId: string) { + if (await shouldMigrateProjectUnderOrganization()) { + await migrateProjectsIntoOrganization({ + personalOrganizationId, + }); + + const preferredProjectType = localStorage.getItem('prefers-project-type'); + if (preferredProjectType === 'remote') { + const localProjects = await database.find('Project', { + parentId: personalOrganizationId, + remoteId: null, + }); + + // If any of those fail projects will still be under the organization as local projects + for (const project of localProjects) { + updateLocalProjectToRemote({ + project, + organizationId: personalOrganizationId, + sessionId, + vcs: VCSInstance(), + }); + } + } + } +}; + export const indexLoader: LoaderFunction = async () => { + console.log('org index loader'); const { id: sessionId, accountId } = await userSession.getOrCreate(); if (sessionId) { await syncOrganization(sessionId, accountId); @@ -172,44 +252,18 @@ export const indexLoader: LoaderFunction = async () => { const organizations = JSON.parse(localStorage.getItem(`${accountId}:organizations`) || '[]') as Organization[]; invariant(organizations, 'Failed to fetch organizations.'); - const personalOrganization = organizations.filter(isPersonalOrganization) - .find(organization => - isOwnerOfOrganization({ - organization, - accountId, - })); - invariant(personalOrganization, 'Failed to find personal organization your account appears to be in an invalid state. Please contact support if this is a recurring issue.'); - if (await shouldMigrateProjectUnderOrganization()) { - await migrateProjectsIntoOrganization({ - personalOrganization, - }); + const personalOrganization = findPersonalOrganization(organizations, accountId); + invariant(personalOrganization, 'Failed to find personal organization your account appears to be in an invalid state. Please contact support if this is a recurring issue.'); + const personalOrganizationId = personalOrganization.id; + await migrateProjectsUnderOrganization(personalOrganizationId, sessionId); - const preferredProjectType = localStorage.getItem('prefers-project-type'); - if (preferredProjectType === 'remote') { - const localProjects = await database.find('Project', { - parentId: personalOrganization.id, - remoteId: null, - }); - - // If any of those fail projects will still be under the organization as local projects - for (const project of localProjects) { - updateLocalProjectToRemote({ - project, - organizationId: personalOrganization.id, - sessionId, - vcs: VCSInstance(), - }); - } - } - } - - if (personalOrganization) { - return redirect(`/organization/${personalOrganization.id}`); - } + if (personalOrganization) { + return redirect(`/organization/${personalOrganizationId}`); + } - if (organizations.length > 0) { - return redirect(`/organization/${organizations[0].id}`); - } + if (organizations.length > 0) { + return redirect(`/organization/${organizations[0].id}`); + } } await session.logout(); @@ -233,6 +287,7 @@ export interface OrganizationLoaderData { } export const loader: LoaderFunction = async () => { + console.log('org loader'); const { id, accountId } = await userSession.getOrCreate(); if (id) { const organizations = JSON.parse(localStorage.getItem(`${accountId}:organizations`) || '[]') as Organization[]; @@ -402,6 +457,12 @@ const OrganizationRoute = () => { }; const [status, setStatus] = useState<'online' | 'offline'>('online'); + useAsyncTask({ + organizationId, + organizations, + userSession, + }); + useEffect(() => { const isIdleAndUninitialized = untrackedProjectsFetcher.state === 'idle' && !untrackedProjectsFetcher.data; if (isIdleAndUninitialized) { diff --git a/packages/insomnia/src/ui/routes/project.tsx b/packages/insomnia/src/ui/routes/project.tsx index 3dcd8c6a217..ad47e2c4f97 100644 --- a/packages/insomnia/src/ui/routes/project.tsx +++ b/packages/insomnia/src/ui/routes/project.tsx @@ -1,5 +1,5 @@ import { IconName } from '@fortawesome/fontawesome-svg-core'; -import React, { FC, Fragment, useEffect, useState } from 'react'; +import React, { FC, Fragment, useEffect, useMemo, useState } from 'react'; import { Button, Dialog, @@ -81,6 +81,7 @@ import { MockServerSettingsModal } from '../components/modals/mock-server-settin import { EmptyStatePane } from '../components/panes/project-empty-state-pane'; import { TimeFromNow } from '../components/time-from-now'; import { useInsomniaEventStreamContext } from '../context/app/insomnia-event-stream-context'; +import { useLoaderDeferData } from '../hooks/use-loader-defer-data'; import { OrganizationFeatureLoaderData, useOrganizationLoaderData } from './organization'; import { useRootLoaderData } from './root'; @@ -202,20 +203,29 @@ async function syncTeamProjects({ })); } +export const syncProjects = async (organizationId: string) => { + const user = await models.userSession.getOrCreate(); + const teamProjects = await getAllTeamProjects(organizationId); + // ensure we don't sync projects in the wrong place + if (teamProjects.length > 0 && user.id && !isScratchpadOrganizationId(organizationId)) { + await syncTeamProjects({ + organizationId, + teamProjects, + }); + } +}; + export const syncProjectsAction: ActionFunction = async ({ params }) => { const { organizationId } = params; invariant(organizationId, 'Organization ID is required'); - const teamProjects = await getAllTeamProjects(organizationId); - await syncTeamProjects({ - organizationId, - teamProjects, - }); + await syncProjects(organizationId); return null; }; export const indexLoader: LoaderFunction = async ({ params }) => { + console.log('org id index loader'); const { organizationId } = params; invariant(organizationId, 'Organization ID is required'); @@ -224,18 +234,8 @@ export const indexLoader: LoaderFunction = async ({ params }) => { `locationHistoryEntry:${organizationId}` ); - let teamProjects: TeamProject[] = []; - try { - const user = await models.userSession.getOrCreate(); - teamProjects = await getAllTeamProjects(organizationId); - // ensure we don't sync projects in the wrong place - if (teamProjects.length > 0 && user.id && !isScratchpadOrganizationId(organizationId)) { - await syncTeamProjects({ - organizationId, - teamProjects, - }); - } + await syncProjects(organizationId); } catch (err) { console.log('Could not fetch remote projects.'); } @@ -297,7 +297,7 @@ export interface ProjectIdLoaderData { } export interface ProjectLoaderData { - files: InsomniaFile[]; + localFiles: InsomniaFile[]; allFilesCount: number; documentsCount: number; collectionsCount: number; @@ -305,13 +305,8 @@ export interface ProjectLoaderData { projectsCount: number; activeProject?: Project; projects: Project[]; - learningFeature: { - active: boolean; - title: string; - message: string; - cta: string; - url: string; - }; + learningFeaturePromise?: Promise; + remoteFilesPromise?: Promise; } async function getAllLocalFiles({ @@ -489,6 +484,7 @@ export const listWorkspacesLoader: LoaderFunction = async ({ params }): Promise< }; export const projectIdLoader: LoaderFunction = async ({ params }): Promise => { + console.log('project id loader'); const { projectId } = params; invariant(projectId, 'Project ID is required'); @@ -534,6 +530,7 @@ const getLearningFeature = async (fallbackLearningFeature: LearningFeature) => { export const loader: LoaderFunction = async ({ params, }): Promise => { + console.log('project id index loader'); const { organizationId, projectId } = params; invariant(organizationId, 'Organization ID is required'); const { id: sessionId } = await userSession.getOrCreate(); @@ -546,7 +543,7 @@ export const loader: LoaderFunction = async ({ }; if (!projectId) { return { - files: [], + localFiles: [], allFilesCount: 0, documentsCount: 0, collectionsCount: 0, @@ -554,7 +551,6 @@ export const loader: LoaderFunction = async ({ projectsCount: 0, activeProject: undefined, projects: [], - learningFeature: fallbackLearningFeature, }; } @@ -568,33 +564,33 @@ export const loader: LoaderFunction = async ({ const project = await models.project.getById(projectId); invariant(project, `Project was not found ${projectId}`); - const [localFiles, remoteFiles, organizationProjects = [], learningFeature] = await Promise.all([ + const [localFiles, organizationProjects = []] = await Promise.all([ getAllLocalFiles({ projectId }), - getAllRemoteFiles({ projectId, organizationId }), database.find(models.project.type, { parentId: organizationId, }), - getLearningFeature(fallbackLearningFeature), ]); - const files = [...localFiles, ...remoteFiles]; + const remoteFilesPromise = getAllRemoteFiles({ projectId, organizationId }); + const learningFeaturePromise = getLearningFeature(fallbackLearningFeature); const projects = sortProjects(organizationProjects); return { - files, - learningFeature, + localFiles, + learningFeaturePromise, + remoteFilesPromise, projects, projectsCount: organizationProjects.length, activeProject: project, - allFilesCount: files.length, - documentsCount: files.filter( + allFilesCount: localFiles.length, + documentsCount: localFiles.filter( file => file.scope === 'design' ).length, - collectionsCount: files.filter( + collectionsCount: localFiles.filter( file => file.scope === 'collection' ).length, - mockServersCount: files.filter( + mockServersCount: localFiles.filter( file => file.scope === 'mock-server' ).length, }; @@ -602,7 +598,7 @@ export const loader: LoaderFunction = async ({ const ProjectRoute: FC = () => { const { - files, + localFiles, activeProject, projects, allFilesCount, @@ -610,13 +606,21 @@ const ProjectRoute: FC = () => { mockServersCount, documentsCount, projectsCount, - learningFeature, + learningFeaturePromise, + remoteFilesPromise, } = useLoaderData() as ProjectLoaderData; const [isLearningFeatureDismissed, setIsLearningFeatureDismissed] = useLocalStorage('learning-feature-dismissed', ''); const { organizationId, projectId } = useParams() as { organizationId: string; projectId: string; }; + const [learningFeature] = useLoaderDeferData(learningFeaturePromise); + const [remoteFiles] = useLoaderDeferData(remoteFilesPromise); + + const finalFiles = useMemo(() => { + console.log(remoteFiles, 'remoteFiles'); + return remoteFiles ? [...localFiles, ...remoteFiles] : localFiles; + }, [localFiles, remoteFiles]); const { userSession } = useRootLoaderData(); const pullFileFetcher = useFetcher(); @@ -653,7 +657,7 @@ const ProjectRoute: FC = () => { const isUserOwner = organization && userSession.accountId && isOwnerOfOrganization({ organization, accountId: userSession.accountId }); const isPersonalOrg = organization && isPersonalOrganization(organization); - const filteredFiles = files + const filteredFiles = finalFiles .filter(w => (workspaceListScope !== 'all' ? w.scope === workspaceListScope : true)) .filter(workspace => workspaceListFilter @@ -1103,7 +1107,7 @@ const ProjectRoute: FC = () => { }} )} - {!isLearningFeatureDismissed && learningFeature.active && ( + {!isLearningFeatureDismissed && learningFeature?.active && (
@@ -1267,7 +1271,7 @@ const ProjectRoute: FC = () => { items={filesWithPresence} onAction={id => { // hack to workaround gridlist not have access to workspace scope - const file = files.find(f => f.id === id); + const file = finalFiles.find(f => f.id === id); invariant(file, 'File not found'); if (file.scope === 'unsynced') { if (activeProject?.remoteId && file.remoteId) { diff --git a/packages/insomnia/src/ui/routes/workspace.tsx b/packages/insomnia/src/ui/routes/workspace.tsx index 68bb027309a..07c47c15ddf 100644 --- a/packages/insomnia/src/ui/routes/workspace.tsx +++ b/packages/insomnia/src/ui/routes/workspace.tsx @@ -65,6 +65,7 @@ export const workspaceLoader: LoaderFunction = async ({ request, params, }): Promise => { + console.log('worker space data'); const { organizationId, projectId, workspaceId } = params; invariant(organizationId, 'Organization ID is required'); invariant(projectId, 'Project ID is required'); diff --git a/packages/insomnia/src/utils/router.ts b/packages/insomnia/src/utils/router.ts new file mode 100644 index 00000000000..49463c4fd1c --- /dev/null +++ b/packages/insomnia/src/utils/router.ts @@ -0,0 +1,91 @@ +import { matchPath } from 'react-router-dom'; + +import { database } from '../common/database'; +import * as models from '../models'; +import { Organization } from '../models/organization'; +import { findPersonalOrganization } from '../models/organization'; +import { Project } from '../models/project'; +import { AsyncTask } from '../ui/routes/organization'; + +// generate as complete a path as possible, reduce router redirects, and let all loader run parallel +export const getWholePath = async (accountId: string) => { + const organizations = JSON.parse(localStorage.getItem(`${accountId}:organizations`) || '[]') as Organization[]; + const personalOrganization = findPersonalOrganization(organizations, accountId); + if (!personalOrganization) { + return '/organization'; + } + + const personalOrganizationId = personalOrganization.id; + // When org icon is clicked this ensures we remember the last visited page + const prevOrganizationLocation = localStorage.getItem( + `locationHistoryEntry:${personalOrganizationId}` + ); + + // Check if the last visited project exists and redirect to it + if (prevOrganizationLocation) { + const match = matchPath( + { + path: '/organization/:organizationId/project/:projectId', + end: false, + }, + prevOrganizationLocation + ); + + if (match && match.params.organizationId && match.params.projectId) { + const existingProject = await models.project.getById(match.params.projectId); + + if (existingProject) { + console.log('Redirecting to last visited project', existingProject._id); + return `/organization/${match?.params.organizationId}/project/${existingProject._id}`; + } + } + } + const allOrganizationProjects = await database.find(models.project.type, { + parentId: personalOrganizationId, + }) || []; + + // Check if the org has any projects and redirect to the first one + const projectId = allOrganizationProjects[0]?._id; + + if (!projectId) { + return `/organization/${personalOrganizationId}/project`; + } + + return `/organization/${personalOrganizationId}/project/${projectId}`; +}; + +export const getInitialEntry = async () => { + // If the user has not seen the onboarding, then show it + // Otherwise if the user is not logged in and has not logged in before, then show the login + // Otherwise if the user is logged in, then show the organization + try { + const hasSeenOnboardingV9 = Boolean(window.localStorage.getItem('hasSeenOnboardingV9')); + + if (!hasSeenOnboardingV9) { + return '/onboarding'; + } + + const hasUserLoggedInBefore = window.localStorage.getItem('hasUserLoggedInBefore'); + + const user = await models.userSession.getOrCreate(); + if (user.id) { + // return '/organization'; + const path = await getWholePath(user.accountId); + return { + pathname: path, + state: { + // async task need to excute when fisrt entry + asyncTaskList: [AsyncTask.SyncOrganization, AsyncTask.MigrateProjects, AsyncTask.SyncProjects], + }, + }; + } + + if (hasUserLoggedInBefore) { + return '/auth/login'; + } + + return '/organization/org_scratchpad/project/proj_scratchpad/workspace/wrk_scratchpad/debug'; + } catch (e) { + return '/organization/org_scratchpad/project/proj_scratchpad/workspace/wrk_scratchpad/debug'; + } +};