Skip to content

Commit 86053a8

Browse files
authored
Open project improvements (#13778)
Fixes #13775 https://github.com/user-attachments/assets/b6fe118f-1bff-4bf7-a89d-901144de5ef3 (there's now no moment of dashboard being visible before opening Welcome Project)
1 parent 9892810 commit 86053a8

File tree

5 files changed

+121
-49
lines changed

5 files changed

+121
-49
lines changed

app/gui/src/components/AppContainer/AppContainerInner.vue

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import type { PaywallFeatureName } from '#/hooks/billing'
33
import { UserBar as UserBarReact } from '#/pages/dashboard/UserBar'
44
import { BackendType, EnsoPath, type ProjectId } from '#/services/Backend'
5-
import { useContainerData, type LaunchedProject, type TabId } from '$/providers/container'
5+
import {
6+
OpenedProject,
7+
useContainerData,
8+
type LaunchedProject,
9+
type TabId,
10+
} from '$/providers/container'
611
import { RightPanelDataProviderForReact } from '$/providers/react/container'
712
import { provideRightPanelData } from '$/providers/rightPanel'
813
import { appContainerBindings } from '@/bindings'
@@ -41,7 +46,7 @@ const emit = defineEmits<{
4146
// with veaury's ref assignment implementation that runs during parent React component lifecycle.
4247
const fullscreenRoot = shallowRef<HTMLElement>()
4348
44-
const { tab, openedProjects } = toRefs(useContainerData())
49+
const { tab, openingProjects, openedProjects } = toRefs(useContainerData())
4550
provideRightPanelData(tab, props.isFeatureUnderPaywall)
4651
provideFullscreenRoot(fullscreenRoot)
4752
@@ -57,12 +62,22 @@ function setProjectReady(project: ProjectId, projectTab: TabId, ready: boolean)
5762
}
5863
}
5964
60-
function loadingProjectSpinnerPhase(project: LaunchedProject) {
61-
return project.hybrid != null || project.type === BackendType.local ?
65+
function loadingProjectSpinnerPhase(project: OpenedProject) {
66+
return (
67+
project.state === 'launched' && (project.hybrid != null || project.type === BackendType.local)
68+
) ?
6269
'loading-fast'
6370
: 'loading-slow'
6471
}
6572
73+
function closeOpenedProject(project: OpenedProject) {
74+
if (project.state === 'launched') {
75+
emit('closeProject', project)
76+
} else {
77+
openingProjects.value.delete(project.id)
78+
}
79+
}
80+
6681
watch(openedProjects, (openedProjectsList) => {
6782
const openedProjectsSet = new Set(openedProjectsList.map((proj) => proj.id))
6883
for (const proj of readyProjects) {
@@ -92,7 +107,7 @@ function closeTab() {
92107
default: {
93108
// project id
94109
const project = openedProjects.value.find((proj) => proj.ensoPath === tab.value)
95-
if (project) emit('closeProject', project)
110+
if (project) closeOpenedProject(project)
96111
break
97112
}
98113
}
@@ -142,7 +157,7 @@ const onSignOut = () => {
142157
:icon="readyProjects.has(project.id) ? 'graph_editor' : undefined"
143158
:label="projectNames.get(project.id)"
144159
@update:selected="$event && (tab = EnsoPath(project.ensoPath))"
145-
@close="emit('closeProject', project)"
160+
@close="closeOpenedProject(project)"
146161
>
147162
<GrowingSpinner
148163
v-if="!readyProjects.has(project.id)"
@@ -166,19 +181,21 @@ const onSignOut = () => {
166181
<KeepAlive>
167182
<Drive v-if="tab === 'drive'" />
168183
</KeepAlive>
169-
<div
170-
v-for="project in openedProjects"
171-
:key="project.id"
172-
class="editor"
173-
:class="{ hidden: !project.shown.value }"
174-
>
175-
<Editor
176-
:hidden="!project.shown.value"
177-
:project="project"
178-
@readyUpdate="setProjectReady(project.id, EnsoPath(project.ensoPath), $event)"
179-
@nameUpdate="projectNames.set(project.id, $event)"
180-
/>
181-
</div>
184+
<template v-for="project in openedProjects">
185+
<div
186+
v-if="project.state === 'launched'"
187+
:key="project.id"
188+
class="editor"
189+
:class="{ hidden: !project.shown.value }"
190+
>
191+
<Editor
192+
:hidden="!project.shown.value"
193+
:project="project"
194+
@readyUpdate="setProjectReady(project.id, EnsoPath(project.ensoPath), $event)"
195+
@nameUpdate="projectNames.set(project.id, $event)"
196+
/>
197+
</div>
198+
</template>
182199

183200
<KeepAlive>
184201
<Settings v-if="tab === 'settings'" />

app/gui/src/dashboard/hooks/projectHooks.ts

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ import type { LaunchedProject, LaunchedProjectId } from '$/providers/container'
1010
import * as authProvider from '$/providers/react'
1111
import {
1212
useAddLaunchedProject,
13+
useAddOpeningProject,
1314
useContainerData,
1415
useRemoveLaunchedProject,
16+
useRemoveOpeningProject,
1517
useUpdateLaunchedProjects,
1618
} from '$/providers/react/container'
1719

1820
import { useCanRunProjects } from '#/hooks/backendHooks'
1921
import { useUploadFileMutation } from '#/hooks/backendUploadFilesHooks'
2022
import { useToastAndLog } from '#/hooks/toastAndLogHooks'
21-
import { useAddOpeningProject, useRemoveOpeningProject } from '#/providers/ProjectsProvider/hooks'
2223
import type Backend from '#/services/Backend'
2324
import * as backendModule from '#/services/Backend'
2425
import { useBackends } from '$/providers/react'
@@ -206,7 +207,7 @@ createGetProjectDetailsQuery.getQueryKey = (id: LaunchedProjectId) => ['project'
206207

207208
const OPEN_PROJECT_MUTATION_KEY = ['openProject'] as const
208209

209-
/** A mutation to open a project. */
210+
/** A mutation to open a project in backend. */
210211
export function useOpenProjectMutation() {
211212
const client = reactQuery.useQueryClient()
212213
const session = authProvider.useFullUserSession()
@@ -225,8 +226,9 @@ export function useOpenProjectMutation() {
225226
hybrid,
226227
inBackground = false,
227228
suppressHybridProjectOpen: _ = false,
229+
ensoPath,
228230
}: LaunchedProject & { inBackground?: boolean; suppressHybridProjectOpen?: boolean }) => {
229-
addOpeningProject(hybrid?.cloudProjectId ?? id)
231+
addOpeningProject(hybrid?.cloudProjectId ?? id, ensoPath)
230232
const backend = type === backendModule.BackendType.remote ? remoteBackend : localBackend
231233

232234
invariant(backend != null, 'Backend is null')
@@ -460,7 +462,7 @@ function useOpenProject() {
460462
const queryKey = createGetProjectDetailsQuery.getQueryKey(project.id)
461463
client.setQueryData(queryKey, { state: { type: backendModule.ProjectState.openInProgress } })
462464

463-
addOpeningProject(project.hybrid?.cloudProjectId ?? project.id)
465+
addOpeningProject(project.hybrid?.cloudProjectId ?? project.id, project.ensoPath)
464466

465467
if (!enableMultitabs) {
466468
// Since multiple tabs cannot be opened at the same time, the opened projects need to be closed first.
@@ -516,7 +518,7 @@ function useOpenHybridProject() {
516518

517519
try {
518520
invariant(localBackend != null, 'Local Backend is null')
519-
addOpeningProject(asset.id)
521+
addOpeningProject(asset.id, asset.ensoPath)
520522
const projectSessionId = await remoteBackend.setHybridOpenInProgress(asset.id, asset.title)
521523
const localProject = await remoteBackend.downloadProject(asset.id)
522524
const cloudProjectDirectoryPath = backendModule.EnsoPath(
@@ -537,7 +539,6 @@ function useOpenHybridProject() {
537539
}
538540
}
539541

540-
removeOpeningProject(asset.id)
541542
invariant(project, 'Downloaded cloud project does not exist in Local Backend.')
542543
launchedProject = {
543544
id: project.id,
@@ -555,12 +556,13 @@ function useOpenHybridProject() {
555556
}
556557
await openProject(launchedProject)
557558
} catch (error) {
558-
removeOpeningProject(asset.id)
559559
toastAndLog('openProjectError', error, asset.title)
560560
await Promise.allSettled([
561561
closeProject({ ...asset, type: backendModule.BackendType.remote }),
562562
...(launchedProject ? [closeProject(launchedProject)] : []),
563563
])
564+
} finally {
565+
removeOpeningProject(asset.id)
564566
}
565567
},
566568
)
@@ -667,6 +669,7 @@ export function useCloseAllProjects() {
667669
const closeProject = useCloseProject()
668670
const containerData = useContainerData()
669671
const removeLaunchedProject = useRemoveLaunchedProject()
672+
const removeOpeningProject = useRemoveOpeningProject()
670673
const { remoteBackend, localBackend } = useBackends()
671674
const ensureQueryData = useEnsureQueryData()
672675

@@ -675,21 +678,25 @@ export function useCloseAllProjects() {
675678

676679
await Promise.all(
677680
launchedProjects.map(async (project) => {
678-
const backend =
679-
project.type === backendModule.BackendType.remote || project.hybrid != null ?
680-
remoteBackend
681-
: localBackend
682-
invariant(backend != null, 'Backend must not be async null')
683-
const projectDetails = await ensureQueryData(
684-
createGetProjectDetailsQuery({
685-
assetId: project.hybrid != null ? project.hybrid.cloudProjectId : project.id,
686-
backend,
687-
}),
688-
)
689-
if (backendModule.IS_OPENING_OR_OPENED[projectDetails.state.type]) {
690-
await closeProject(project)
681+
if (project.state === 'launched') {
682+
const backend =
683+
project.type === backendModule.BackendType.remote || project.hybrid != null ?
684+
remoteBackend
685+
: localBackend
686+
invariant(backend != null, 'Backend must not be async null')
687+
const projectDetails = await ensureQueryData(
688+
createGetProjectDetailsQuery({
689+
assetId: project.hybrid != null ? project.hybrid.cloudProjectId : project.id,
690+
backend,
691+
}),
692+
)
693+
if (backendModule.IS_OPENING_OR_OPENED[projectDetails.state.type]) {
694+
await closeProject(project)
695+
} else {
696+
removeLaunchedProject(project.id)
697+
}
691698
} else {
692-
removeLaunchedProject(project.id)
699+
removeOpeningProject(project.id)
693700
}
694701
}),
695702
)

app/gui/src/dashboard/pages/dashboard/Dashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function Dashboard(props: DashboardProps) {
6868
(lp) => lp.id === props.projectToOpen?.asset.id,
6969
)
7070
const initialAlreadyLaunchedHybridProject = launchedProjects.find(
71-
(lp) => lp.hybrid?.cloudProjectId === props.projectToOpen?.asset.id,
71+
(lp) => lp.state === 'launched' && lp.hybrid?.cloudProjectId === props.projectToOpen?.asset.id,
7272
)
7373

7474
usePrefetchQuery({

app/gui/src/providers/container.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import LocalStorage from '#/utilities/LocalStorage'
33
import { createContextStore } from '@/providers'
44
import { proxyRefs } from '@/util/reactivity'
55
import { normalizeRouteParamToString } from '@/util/router'
6-
import { computed } from 'vue'
6+
import { computed, reactive, Ref } from 'vue'
77
import { useRoute, useRouter } from 'vue-router'
88
import * as z from 'zod'
99

@@ -55,6 +55,22 @@ LocalStorage.registerKey('launchedProjects', {
5555
schema: LAUNCHED_PROJECT_SCHEMA,
5656
})
5757

58+
/**
59+
* A project opened by user
60+
*
61+
* State "opening" means that we still do some processing before actually opening
62+
* (like downloading project for hybrid run). Usually we don't have all information to construct
63+
* {@link LaunchedProject} at this stage.
64+
*
65+
* State "launched" is a state where {@link LaunchedProject} is available. The project may still
66+
* be initializing, though.
67+
*/
68+
// TODO[ao]: this is convoluted and shall be improved in https://github.com/enso-org/enso/issues/13491
69+
export type OpenedProject = (
70+
| { state: 'opening'; id: ProjectId; ensoPath: string }
71+
| ({ state: 'launched' } & LaunchedProject)
72+
) & { shown: Ref<boolean> }
73+
5874
/** Tab identifier, equal to the path of the view's URL. */
5975
export type TabId = 'drive' | 'settings' | EnsoPath
6076

@@ -78,13 +94,27 @@ export const [provideContainerData, useContainerData] = createContextStore(
7894
const route = useRoute()
7995
const localStorage = LocalStorage.getInstance()
8096

81-
const openedProjects = computed(
82-
() =>
83-
localStorage.get('launchedProjects')?.map((lp) => ({
84-
...lp,
85-
shown: computed(() => tab.value === lp.ensoPath),
86-
})) ?? [],
87-
)
97+
const launchedProjects = computed(() => localStorage.get('launchedProjects') ?? [])
98+
99+
const openingProjects = reactive(new Map<ProjectId, EnsoPath>())
100+
101+
const openedProjects = computed<OpenedProject[]>(() => {
102+
const launched = launchedProjects.value.map(
103+
(project) => ({ state: 'launched', ...project }) as Omit<OpenedProject, 'shown'>,
104+
)
105+
const opened = [...openingProjects.entries()]
106+
.map(
107+
([id, ensoPath]) => ({ state: 'opening', id, ensoPath }) as Omit<OpenedProject, 'shown'>,
108+
)
109+
.filter(({ ensoPath }) => launched.find((project) => project.ensoPath === ensoPath) == null)
110+
return launched.concat(opened).map(
111+
(project) =>
112+
({
113+
...project,
114+
shown: computed(() => tab.value === project.ensoPath),
115+
}) as OpenedProject,
116+
)
117+
})
88118

89119
const isValidTab = (name: string | undefined): name is TabId =>
90120
name === 'drive' ||
@@ -119,6 +149,7 @@ export const [provideContainerData, useContainerData] = createContextStore(
119149

120150
return proxyRefs({
121151
openedProjects,
152+
openingProjects,
122153
tab,
123154
addLaunchedProject,
124155
removeLaunchedProject,

app/gui/src/providers/react/container.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEventCallback } from '#/hooks/eventCallbackHooks'
2+
import { EnsoPath, ProjectId } from '#/services/Backend'
23
import { ContainerData, useContainerData as useContainerDataVue } from '$/providers/container'
34
import { RightPanelData, useRightPanelData as useRightPanelDataVue } from '$/providers/rightPanel'
45
import { reactComponent } from '@/util/react'
@@ -88,3 +89,19 @@ export function useClearLaunchedProjects() {
8889
updateLaunchedProjects(() => [])
8990
})
9091
}
92+
93+
/** A function to add project to "opening projects" list */
94+
export function useAddOpeningProject() {
95+
const { openingProjects } = useContainerData()
96+
return useEventCallback((id: ProjectId, ensoPath: string) => {
97+
openingProjects.set(id, EnsoPath(ensoPath))
98+
})
99+
}
100+
101+
/** A function to remove project from "opening projects" list */
102+
export function useRemoveOpeningProject() {
103+
const { openingProjects } = useContainerData()
104+
return useEventCallback((id: ProjectId) => {
105+
openingProjects.delete(id)
106+
})
107+
}

0 commit comments

Comments
 (0)