diff --git a/app/ide-desktop/eslint.config.js b/app/ide-desktop/eslint.config.js index 9b7bcf76da35b..88429b5f5ea80 100644 --- a/app/ide-desktop/eslint.config.js +++ b/app/ide-desktop/eslint.config.js @@ -425,6 +425,30 @@ export default [ 'no-undef': 'off', }, }, + { + files: [ + 'lib/dashboard/src/**/*.ts', + 'lib/dashboard/src/**/*.mts', + 'lib/dashboard/src/**/*.cts', + 'lib/dashboard/src/**/*.tsx', + 'lib/dashboard/src/**/*.mtsx', + 'lib/dashboard/src/**/*.ctsx', + ], + rules: { + 'no-restricted-properties': [ + 'error', + { + object: 'console', + message: 'Avoid leaving debugging statements when committing code', + }, + { + object: 'hooks', + property: 'useDebugState', + message: 'Avoid leaving debugging statements when committing code', + }, + ], + }, + }, { files: ['**/*.d.ts'], rules: { diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index 6d883ba731ec2..d8d93a52cf927 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -23,10 +23,10 @@ import * as loggerProvider from '../../providers/logger' import * as modalProvider from '../../providers/modal' import PermissionDisplay, * as permissionDisplay from './permissionDisplay' +import ProjectActionButton, * as projectActionButton from './projectActionButton' import ContextMenu from './contextMenu' import ContextMenuEntry from './contextMenuEntry' import Ide from './ide' -import ProjectActionButton from './projectActionButton' import Rows from './rows' import Templates from './templates' import TopBar from './topBar' @@ -249,7 +249,6 @@ function Dashboard(props: DashboardProps) { const [refresh, doRefresh] = hooks.useRefresh() - const [shouldOpenProjectAfterNextLoad, setShouldOpenProjectAfterNextLoad] = react.useState(true) const [onDirectoryNextLoaded, setOnDirectoryNextLoaded] = react.useState< ((assets: backendModule.Asset[]) => void) | null >(() => @@ -271,6 +270,9 @@ function Dashboard(props: DashboardProps) { ) const [nameOfProjectToImmediatelyOpen, setNameOfProjectToImmediatelyOpen] = react.useState(initialProjectName) + const [projectEvent, setProjectEvent] = react.useState( + null + ) const [query, setQuery] = react.useState('') const [loadingProjectManagerDidFail, setLoadingProjectManagerDidFail] = react.useState(false) const [directoryId, setDirectoryId] = react.useState(rootDirectoryId(organization.id)) @@ -310,7 +312,6 @@ function Dashboard(props: DashboardProps) { backendModule.Asset[] >([]) - const shouldCancelOpeningImmediately = nameOfProjectToImmediatelyOpen != null const listingLocalDirectoryAndWillFail = backend.type === backendModule.BackendType.local && loadingProjectManagerDidFail const listingRemoteDirectoryAndWillFail = @@ -377,10 +378,10 @@ function Dashboard(props: DashboardProps) { }, []) react.useEffect(() => { - if (!shouldOpenProjectAfterNextLoad) { - setNameOfProjectToImmediatelyOpen(null) + if (projectEvent != null) { + setProjectEvent(null) } - }, [shouldOpenProjectAfterNextLoad, nameOfProjectToImmediatelyOpen]) + }, [projectEvent]) const openIde = async (projectId: backendModule.ProjectId) => { switchToIdeTab() @@ -389,6 +390,10 @@ function Dashboard(props: DashboardProps) { } } + const closeIde = () => { + setProject(null) + } + const setBackendType = (newBackendType: backendModule.BackendType) => { if (newBackendType !== backend.type) { setIsLoadingAssets(true) @@ -484,9 +489,12 @@ function Dashboard(props: DashboardProps) {
{ - if (event.detail === 2) { + if (event.detail === 2 && event.target === event.currentTarget) { // It is a double click; open the project. - setNameOfProjectToImmediatelyOpen(projectAsset.title) + setProjectEvent({ + type: projectActionButton.ProjectEventType.open, + projectId: projectAsset.id, + }) } else if ( event.ctrlKey && !event.altKey && @@ -516,21 +524,19 @@ function Dashboard(props: DashboardProps) { { - console.log( - 'what open', - nameOfProjectToImmediatelyOpen, - projectBeingOpened.title - ) - setNameOfProjectToImmediatelyOpen(projectBeingOpened.title) + doOpenManually={() => { + setProjectEvent({ + type: projectActionButton.ProjectEventType.open, + projectId: projectAsset.id, + }) }} onClose={() => { - console.log('what close', nameOfProjectToImmediatelyOpen, null) - setNameOfProjectToImmediatelyOpen(null) - setProject(null) + setProjectEvent({ + type: projectActionButton.ProjectEventType.cancelOpeningAll, + }) + closeIde() }} openIde={() => openIde(projectAsset.id)} /> @@ -722,7 +728,17 @@ function Dashboard(props: DashboardProps) { setDirectoryAssets(newDirectoryAssets) setSecretAssets(newSecretAssets) setFileAssets(newFileAssets) - setShouldOpenProjectAfterNextLoad(false) + if (nameOfProjectToImmediatelyOpen != null) { + const projectToLoad = newProjectAssets.find( + projectAsset => projectAsset.title === nameOfProjectToImmediatelyOpen + ) + if (projectToLoad != null) { + setProjectEvent({ + type: projectActionButton.ProjectEventType.open, + projectId: projectToLoad.id, + }) + } + } onDirectoryNextLoaded?.(assets) setOnDirectoryNextLoaded(null) } @@ -797,7 +813,9 @@ function Dashboard(props: DashboardProps) { parentDirectoryId: directoryId, } await backend.createProject(body) - setShouldOpenProjectAfterNextLoad(true) + // `newProject.projectId` cannot be used directly in a `ProjectEvet` as the project + // does not yet exist in the project list. Opening the project would work, but the project + // would display as closed as it would be created after the event is sent. setNameOfProjectToImmediatelyOpen(projectName) doRefresh() } @@ -1001,7 +1019,10 @@ function Dashboard(props: DashboardProps) { event.stopPropagation() const doOpenForEditing = () => { unsetModal() - setNameOfProjectToImmediatelyOpen(projectAsset.title) + setProjectEvent({ + type: projectActionButton.ProjectEventType.open, + projectId: projectAsset.id, + }) } const doOpenAsFolder = () => { // FIXME[sb]: Uncomment once backend support diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectActionButton.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectActionButton.tsx index 80d3545549391..11a944de80a1d 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectActionButton.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectActionButton.tsx @@ -11,6 +11,33 @@ import * as svg from '../../components/svg' // === Types === // ============= +/** Possible types of project state change. */ +export enum ProjectEventType { + open = 'open', + cancelOpeningAll = 'cancelOpeningAll', +} + +/** Properties common to all project state change events. */ +interface ProjectBaseEvent { + type: Type +} + +/** Requests the specified project to be opened. */ +export interface ProjectOpenEvent extends ProjectBaseEvent { + // FIXME: provide projectId instead + /** This must be a name because it may be specified by name on the command line. + * Note that this will not work properly with the cloud backend if there are multiple projects + * with the same name. */ + projectId: backendModule.ProjectId +} + +/** Requests the specified project to be opened. */ +export interface ProjectCancelOpeningAllEvent + extends ProjectBaseEvent {} + +/** Every possible type of project event. */ +export type ProjectEvent = ProjectCancelOpeningAllEvent | ProjectOpenEvent + /** The state of the spinner. It should go from initial, to loading, to done. */ enum SpinnerState { initial = 'initial', @@ -53,12 +80,9 @@ const SPINNER_CSS_CLASSES: Record = { export interface ProjectActionButtonProps { project: backendModule.Asset appRunner: AppRunner | null - /** Whether this Project should open immediately. */ - shouldOpenImmediately: boolean - /** Whether this Project should cancel opening immediately. - * This would happen if another project is being opened immediately instead. */ - shouldCancelOpeningImmediately: boolean - onOpen: (project: backendModule.Asset) => void + event: ProjectEvent | null + /** Called when the project is opened via the {@link ProjectActionButton}. */ + doOpenManually: () => void onClose: () => void openIde: () => void doRefresh: () => void @@ -66,22 +90,13 @@ export interface ProjectActionButtonProps { /** An interactive button displaying the status of a project. */ function ProjectActionButton(props: ProjectActionButtonProps) { - const { - project, - shouldOpenImmediately, - shouldCancelOpeningImmediately, - appRunner, - onOpen, - onClose, - openIde, - doRefresh, - } = props + const { project, event, appRunner, doOpenManually, onClose, openIde, doRefresh } = props const { backend } = backendProvider.useBackend() const [state, setState] = react.useState(null) const [isCheckingStatus, setIsCheckingStatus] = react.useState(false) const [isCheckingResources, setIsCheckingResources] = react.useState(false) - const [spinnerState, setSpinnerState] = react.useState(SpinnerState.done) + const [spinnerState, setSpinnerState] = react.useState(SpinnerState.initial) const [shouldOpenWhenReady, setShouldOpenWhenReady] = react.useState(false) const [toastId, setToastId] = react.useState(null) @@ -102,47 +117,38 @@ function ProjectActionButton(props: ProjectActionButtonProps) { switch (project.projectState.type) { case backendModule.ProjectState.opened: setState(backendModule.ProjectState.openInProgress) - setSpinnerState(SpinnerState.initial) setIsCheckingResources(true) break case backendModule.ProjectState.openInProgress: setState(backendModule.ProjectState.openInProgress) - setSpinnerState(SpinnerState.initial) setIsCheckingStatus(true) break default: // Some functions below set the state to something different to // the backend state. In that case, the state should not be overridden. - setState(previousState => previousState ?? project.projectState.type) + setState(oldState => oldState ?? project.projectState.type) break } }, []) react.useEffect(() => { - // `shouldOpenImmediately` (set to `true` for the relevant project) takes precedence over - // `shouldCancelOpeningImmediately` (set to `true` for all projects, including the relevant - // project). - console.log(project.title, shouldOpenImmediately, shouldCancelOpeningImmediately) - if (shouldOpenImmediately) { - setShouldOpenWhenReady(true) - switch (state) { - case backendModule.ProjectState.opened: { - setIsCheckingResources(true) - break - } - case backendModule.ProjectState.openInProgress: { - setIsCheckingStatus(true) + if (event != null) { + switch (event.type) { + case ProjectEventType.open: { + if (event.projectId !== project.id) { + setShouldOpenWhenReady(false) + } else { + setShouldOpenWhenReady(true) + void openProject() + } break } - default: { - void openProject() - break + case ProjectEventType.cancelOpeningAll: { + setShouldOpenWhenReady(false) } } - } else if (shouldCancelOpeningImmediately) { - setShouldOpenWhenReady(false) } - }, [shouldOpenImmediately, shouldCancelOpeningImmediately]) + }, [event]) react.useEffect(() => { if (shouldOpenWhenReady && state === backendModule.ProjectState.opened) { @@ -156,10 +162,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) { backend.type === backendModule.BackendType.local && project.id !== localBackend.LocalBackend.currentlyOpeningProjectId ) { - setIsCheckingResources(false) - setIsCheckingStatus(false) setState(backendModule.ProjectState.closed) - setSpinnerState(SpinnerState.done) } }, [project, state, localBackend.LocalBackend.currentlyOpeningProjectId]) @@ -251,7 +254,6 @@ function ProjectActionButton(props: ProjectActionButtonProps) { const closeProject = () => { onClose() - console.log(project.title, 'closing...') setShouldOpenWhenReady(false) setState(backendModule.ProjectState.closed) appRunner?.stopApp() @@ -261,16 +263,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) { } const openProject = async () => { - console.log(project.title, 'opening...') - onOpen(project) setState(backendModule.ProjectState.openInProgress) - setSpinnerState(SpinnerState.initial) - // The `requestAnimationFrame` is required so that the completion percentage goes from - // the `initial` fraction to the `loading` fraction, - // rather than starting at the `loading` fraction. - requestAnimationFrame(() => { - setSpinnerState(SpinnerState.loading) - }) switch (backend.type) { case backendModule.BackendType.remote: setToastId(toast.loading(LOADING_MESSAGE)) @@ -280,12 +273,12 @@ function ProjectActionButton(props: ProjectActionButtonProps) { break case backendModule.BackendType.local: await backend.openProject(project.id) - setState(newState => { - if (newState === backendModule.ProjectState.openInProgress) { + setState(oldState => { + if (oldState === backendModule.ProjectState.openInProgress) { doRefresh() return backendModule.ProjectState.opened } else { - return newState + return oldState } }) break @@ -297,16 +290,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) { case backendModule.ProjectState.created: case backendModule.ProjectState.new: case backendModule.ProjectState.closed: - return ( - - ) + return case backendModule.ProjectState.openInProgress: return (