Skip to content

Commit e7df00b

Browse files
MrFlashAccountfarmaazon
authored andcommitted
Fix displayed name in the project tab in hybrid mode (#12805)
1 parent 22598f5 commit e7df00b

File tree

5 files changed

+97
-67
lines changed

5 files changed

+97
-67
lines changed

app/common/src/text/english.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"ellipsis": "...",
3+
"projectTabBarErrorTitle": "Error loading project",
34
"arbitraryMutationError": "An error occurred while performing an action. Please try again.",
45
"arbitraryFetchError": "An error occurred while fetching data",
56
"arbitraryFetchImageError": "An error occurred while fetching an image",
@@ -299,7 +300,6 @@
299300
"forward": "Forward",
300301
"more": "More",
301302
"close": "Close",
302-
303303
"snowflakeCredentialType": "Snowflake",
304304
"snowflakeCredentialAccount": "Account",
305305
"snowflakeCredentialClientId": "Client ID",
@@ -320,7 +320,6 @@
320320
"credentialStateWaitingForAuthentication": "Authentication not finished",
321321
"credentialStateExpired": "Expired",
322322
"credentialExpiresAt": "Expires at",
323-
324323
"enterSecretPath": "Enter secret path",
325324
"enterText": "Enter text",
326325
"enterNumber": "Enter number",

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

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -256,24 +256,18 @@ export function useOpenProjectMutation() {
256256
...asset,
257257
projectState: { ...asset.projectState, type: backendModule.ProjectState.openInProgress },
258258
}))
259-
260-
void client.cancelQueries({ queryKey })
261259
},
262-
onSuccess: async (_, { type, id, title, parentId, hybrid }) => {
260+
onSuccess: async (_, { title, hybrid }) => {
261+
await client.cancelQueries({ queryKey: ['project'] })
263262
if (hybrid) {
264263
await remoteBackend.setHybridOpened(hybrid.cloudProjectId, title)
265264
}
266-
await client.resetQueries({ queryKey: createGetProjectDetailsQuery.getQueryKey(id) })
267-
await client.invalidateQueries({ queryKey: [type, 'listDirectory', parentId] })
268265
},
269-
onError: async (_, { type, id, parentId }) => {
270-
await client.invalidateQueries({ queryKey: createGetProjectDetailsQuery.getQueryKey(id) })
266+
onError: async (_, { type, parentId }) => {
267+
await client.invalidateQueries({ queryKey: ['project'] })
271268
await client.invalidateQueries({ queryKey: [type, 'listDirectory', parentId] })
272269
},
273-
meta: {
274-
invalidates: [['listDirectory']],
275-
awaitInvalidates: true,
276-
},
270+
meta: { invalidates: [['listDirectory', 'project']], awaitInvalidates: true },
277271
})
278272
}
279273

@@ -374,17 +368,20 @@ export function useCloseProjectMutation() {
374368
/** Mutation to rename a project. */
375369
export function useRenameProjectMutation() {
376370
const client = reactQuery.useQueryClient()
377-
const remoteBackend = backendProvider.useRemoteBackend()
378-
const localBackend = backendProvider.useLocalBackend()
379371
const updateLaunchedProjects = useUpdateLaunchedProjects()
380372

381373
return reactQuery.useMutation({
382374
mutationKey: ['renameProject'],
383-
mutationFn: ({ newName, project }: { newName: string; project: LaunchedProject }) => {
384-
const { type, id, title } = project
385-
const backend = type === backendModule.BackendType.remote ? remoteBackend : localBackend
386-
387-
invariant(backend != null, 'Backend is null')
375+
mutationFn: ({
376+
newName,
377+
project,
378+
backend,
379+
}: {
380+
newName: string
381+
project: LaunchedProject
382+
backend: Backend
383+
}) => {
384+
const { id, title } = project
388385

389386
return backend.updateProject(id, { projectName: newName }, title)
390387
},

app/gui/src/dashboard/layouts/Editor.tsx

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as suspense from '#/components/Suspense'
44
import { useEventCallback } from '#/hooks/eventCallbackHooks'
55
import * as gtagHooks from '#/hooks/gtagHooks'
66
import * as projectHooks from '#/hooks/projectHooks'
7+
import { useTimeoutCallback } from '#/hooks/timeoutHooks'
78
import * as backendProvider from '#/providers/BackendProvider'
89
import type { LaunchedProject } from '#/providers/ProjectsProvider'
910
import * as textProvider from '#/providers/TextProvider'
@@ -12,7 +13,7 @@ import * as twMerge from '#/utilities/tailwindMerge'
1213
import { vueComponent } from '#/utilities/vue'
1314
import * as reactQuery from '@tanstack/react-query'
1415
import * as React from 'react'
15-
import { useTimeoutCallback } from '../hooks/timeoutHooks'
16+
import invariant from 'tiny-invariant'
1617

1718
const ProjectViewTab = React.lazy(() =>
1819
import('@/ProjectViewTab.vue').then(({ default: vue }) => vueComponent(vue)),
@@ -34,10 +35,11 @@ export interface EditorProps {
3435
}
3536

3637
/** The container that launches the IDE. */
37-
function Editor(props: EditorProps) {
38+
export default function Editor(props: EditorProps) {
3839
const { project, hidden, startProject, isOpeningFailed, openingError } = props
3940

4041
const backend = backendProvider.useBackendForProjectType(project.type)
42+
const remoteBackend = backendProvider.useRemoteBackend()
4143

4244
const projectStatusQuery = projectHooks.createGetProjectDetailsQuery({
4345
assetId: project.id,
@@ -46,6 +48,8 @@ function Editor(props: EditorProps) {
4648

4749
const queryClient = reactQuery.useQueryClient()
4850

51+
const isHybrid = project.hybrid != null
52+
4953
const projectQuery = reactQuery.useSuspenseQuery({
5054
...projectStatusQuery,
5155
select: (data) => {
@@ -58,6 +62,17 @@ function Editor(props: EditorProps) {
5862
},
5963
})
6064

65+
// If it's a hybrid project, we need to fetch the project details from the remote backend.
66+
const {
67+
data: { name },
68+
} = reactQuery.useSuspenseQuery({
69+
...projectHooks.createGetProjectDetailsQuery({
70+
assetId: isHybrid ? project.hybrid.cloudProjectId : project.id,
71+
backend: isHybrid ? remoteBackend : backend,
72+
}),
73+
select: (projectDetails) => ({ name: projectDetails.name }),
74+
})
75+
6176
const { isProjectClosed, isProjectOpening, isProjectOpened, isProjectClosing } = projectQuery.data
6277

6378
React.useEffect(() => {
@@ -121,6 +136,7 @@ function Editor(props: EditorProps) {
121136
{...props}
122137
openedProject={projectQuery.data}
123138
backendType={project.type}
139+
projectName={name}
124140
/>
125141
)
126142

@@ -136,11 +152,12 @@ function Editor(props: EditorProps) {
136152
interface EditorInternalProps extends Omit<EditorProps, 'project'> {
137153
readonly openedProject: backendModule.Project
138154
readonly backendType: backendModule.BackendType
155+
readonly projectName: string
139156
}
140157

141158
/** An internal editor. */
142159
function EditorInternal(props: EditorInternalProps) {
143-
const { hidden, ydocUrl, renameProject, openedProject, backendType } = props
160+
const { hidden, ydocUrl, renameProject, openedProject, backendType, projectName } = props
144161

145162
const { getText } = textProvider.useText()
146163
const gtagEvent = gtagHooks.useGtagEvent()
@@ -158,48 +175,31 @@ function EditorInternal(props: EditorInternalProps) {
158175
renameProject(newName, openedProject.projectId)
159176
})
160177

161-
const appProps = React.useMemo<ProjectViewTabProps>(() => {
162-
const jsonAddress = openedProject.jsonAddress
163-
const binaryAddress = openedProject.binaryAddress
164-
const ydocAddress = openedProject.ydocAddress ?? ydocUrl ?? ''
165-
const projectBackend =
166-
backendType === backendModule.BackendType.remote ? remoteBackend : localBackend
167-
168-
if (jsonAddress == null) {
169-
throw new Error(getText('noJSONEndpointError'))
170-
} else if (binaryAddress == null) {
171-
throw new Error(getText('noBinaryEndpointError'))
172-
} else {
173-
return {
174-
hidden,
175-
projectViewProps: {
176-
projectId: openedProject.projectId,
177-
projectName: openedProject.packageName,
178-
projectDisplayedName: openedProject.name,
179-
engine: { rpcUrl: jsonAddress, dataUrl: binaryAddress, ydocUrl: ydocAddress },
180-
renameProject: onRenameProject,
181-
projectBackend,
182-
remoteBackend,
183-
},
184-
}
185-
}
186-
}, [
187-
openedProject,
188-
ydocUrl,
189-
getText,
178+
const jsonAddress = openedProject.jsonAddress
179+
const binaryAddress = openedProject.binaryAddress
180+
const ydocAddress = openedProject.ydocAddress ?? ydocUrl ?? ''
181+
const projectBackend =
182+
backendType === backendModule.BackendType.remote ? remoteBackend : localBackend
183+
184+
invariant(jsonAddress != null, getText('noJSONEndpointError'))
185+
invariant(binaryAddress != null, getText('noBinaryEndpointError'))
186+
187+
const appProps = {
190188
hidden,
191-
onRenameProject,
192-
backendType,
193-
localBackend,
194-
remoteBackend,
195-
])
196-
// EsLint does not handle types imported from vue files and their dependences.
197-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
189+
projectViewProps: {
190+
projectId: openedProject.projectId,
191+
projectName: openedProject.packageName,
192+
projectDisplayedName: projectName,
193+
engine: { rpcUrl: jsonAddress, dataUrl: binaryAddress, ydocUrl: ydocAddress },
194+
renameProject: onRenameProject,
195+
projectBackend,
196+
remoteBackend,
197+
},
198+
} as const
199+
198200
const key: string = appProps.projectViewProps.projectId
199201

200202
// Currently the GUI component needs to be fully rerendered whenever the project is changed. Once
201203
// this is no longer necessary, the `key` could be removed.
202204
return <ProjectViewTab key={key} {...appProps} />
203205
}
204-
205-
export default React.memo(Editor)

app/gui/src/dashboard/layouts/TabBar.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import SvgMask from '#/components/SvgMask'
1818
import { AnimatedBackground } from '#/components/AnimatedBackground'
1919
import { Await } from '#/components/Await'
2020
import { useEventCallback } from '#/hooks/eventCallbackHooks'
21-
import { useBackendForProjectType } from '#/providers/BackendProvider'
21+
import { useBackendForProjectType, useRemoteBackend } from '#/providers/BackendProvider'
2222
import { useInputBindings } from '#/providers/InputBindingsProvider'
23+
import { useText } from '#/providers/TextProvider'
2324
import * as sanitizedEventTargets from '#/utilities/sanitizedEventTargets'
2425
import * as tailwindMerge from '#/utilities/tailwindMerge'
2526
import { twJoin } from '#/utilities/tailwindMerge'
@@ -144,8 +145,10 @@ const SPINNER = <StatelessSpinner state="loading-medium" size={16} />
144145
export function ProjectTab(props: ProjectTabProps) {
145146
const { project, onLoadEnd, onClose, icon: iconRaw, ...rest } = props
146147

148+
const { getText } = useText()
147149
const didNotifyOnLoadEnd = React.useRef(false)
148150
const backend = useBackendForProjectType(project.type)
151+
const remoteBackend = useRemoteBackend()
149152

150153
const stableOnLoadEnd = useEventCallback(() => {
151154
onLoadEnd?.(project)
@@ -155,14 +158,27 @@ export function ProjectTab(props: ProjectTabProps) {
155158
onClose?.(project)
156159
})
157160

158-
const { data, isSuccess, isError, promise } = reactQuery.useQuery({
161+
const isHybrid = project.hybrid != null
162+
const projectId = isHybrid ? project.hybrid.cloudProjectId : project.id
163+
164+
const { data, isSuccess, isError } = reactQuery.useQuery({
159165
...projectHooks.createGetProjectDetailsQuery({ assetId: project.id, backend }),
160166
select: (projectDetails) => ({
161-
title: projectDetails.name,
162167
isOpened: projectHooks.OPENED_PROJECT_STATES.has(projectDetails.state.type),
163168
}),
164169
})
165170

171+
// We get title separately because the title differs depending on whenever project is in hybrid mode
172+
// but it's fine, because react-query will deduplicate the queries automatically
173+
const { promise } = reactQuery.useQuery({
174+
...projectHooks.createGetProjectDetailsQuery({
175+
assetId: projectId,
176+
// If it's a hybrid project, we need to fetch the project details from the remote backend.
177+
backend: isHybrid ? remoteBackend : backend,
178+
}),
179+
select: (projectDetails) => ({ title: projectDetails.name }),
180+
})
181+
166182
const isReady = isSuccess && data.isOpened
167183

168184
React.useEffect(() => {
@@ -192,7 +208,11 @@ export function ProjectTab(props: ProjectTabProps) {
192208

193209
return (
194210
<Tab {...rest} icon={icon} onClose={stableOnClose}>
195-
<Await promise={promise} fallback={null}>
211+
<Await
212+
promise={promise}
213+
fallback={<></>}
214+
FallbackComponent={() => getText('projectTabBarErrorTitle')}
215+
>
196216
{({ title }) => title}
197217
</Await>
198218
</Tab>

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import { Suspense } from '#/components/Suspense'
99
import { useEventCallback } from '#/hooks/eventCallbackHooks'
1010
import { useOpenProjectMutation, useRenameProjectMutation } from '#/hooks/projectHooks'
1111
import type { AssetManagementApi } from '#/layouts/AssetsTable'
12+
import { useLocalBackend, useRemoteBackend } from '#/providers/BackendProvider'
1213
import { useLaunchedProjects, usePage } from '#/providers/ProjectsProvider'
13-
import type { ProjectId } from '#/services/Backend'
14+
import { BackendType, type ProjectId } from '#/services/Backend'
1415
import { omit } from 'enso-common/src/utilities/data/object'
1516
import { lazy, type ReactNode } from 'react'
1617
import { Collection } from 'react-aria-components'
18+
import invariant from 'tiny-invariant'
1719

1820
/** The props for the {@link DashboardTabPanels} component. */
1921
export interface DashboardTabPanelsProps {
@@ -35,6 +37,8 @@ export function DashboardTabPanels(props: DashboardTabPanelsProps) {
3537
const launchedProjects = useLaunchedProjects()
3638
const openProjectMutation = useOpenProjectMutation()
3739
const renameProjectMutation = useRenameProjectMutation()
40+
const remoteBackend = useRemoteBackend()
41+
const localBackend = useLocalBackend()
3842

3943
const onRenameProject = useEventCallback(async (newName: string, projectId: ProjectId) => {
4044
const project = launchedProjects.find((proj) => proj.id === projectId)
@@ -43,7 +47,17 @@ export function DashboardTabPanels(props: DashboardTabPanelsProps) {
4347
return
4448
}
4549

46-
await renameProjectMutation.mutateAsync({ newName, project })
50+
const isHybrid = project.hybrid != null
51+
const backendType = isHybrid ? BackendType.remote : project.type
52+
const backend = backendType === BackendType.remote ? remoteBackend : localBackend
53+
const id = isHybrid ? project.hybrid.cloudProjectId : project.id
54+
invariant(backend != null, 'Backend is null')
55+
56+
await renameProjectMutation.mutateAsync({
57+
newName,
58+
backend,
59+
project: { ...project, id },
60+
})
4761
})
4862

4963
const tabPanels = [

0 commit comments

Comments
 (0)