Skip to content

Commit

Permalink
Finally fix spinner bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
somebody1234 committed May 31, 2023
1 parent a7a58d4 commit ae7b693
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 88 deletions.
24 changes: 24 additions & 0 deletions app/ide-desktop/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
>(() =>
Expand All @@ -271,6 +270,9 @@ function Dashboard(props: DashboardProps) {
)
const [nameOfProjectToImmediatelyOpen, setNameOfProjectToImmediatelyOpen] =
react.useState(initialProjectName)
const [projectEvent, setProjectEvent] = react.useState<projectActionButton.ProjectEvent | null>(
null
)
const [query, setQuery] = react.useState('')
const [loadingProjectManagerDidFail, setLoadingProjectManagerDidFail] = react.useState(false)
const [directoryId, setDirectoryId] = react.useState(rootDirectoryId(organization.id))
Expand Down Expand Up @@ -310,7 +312,6 @@ function Dashboard(props: DashboardProps) {
backendModule.Asset<backendModule.AssetType.file>[]
>([])

const shouldCancelOpeningImmediately = nameOfProjectToImmediatelyOpen != null
const listingLocalDirectoryAndWillFail =
backend.type === backendModule.BackendType.local && loadingProjectManagerDidFail
const listingRemoteDirectoryAndWillFail =
Expand Down Expand Up @@ -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()
Expand All @@ -389,6 +390,10 @@ function Dashboard(props: DashboardProps) {
}
}

const closeIde = () => {
setProject(null)
}

const setBackendType = (newBackendType: backendModule.BackendType) => {
if (newBackendType !== backend.type) {
setIsLoadingAssets(true)
Expand Down Expand Up @@ -484,9 +489,12 @@ function Dashboard(props: DashboardProps) {
<div
className="flex text-left items-center align-middle whitespace-nowrap"
onClick={event => {
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 &&
Expand Down Expand Up @@ -516,21 +524,19 @@ function Dashboard(props: DashboardProps) {
<ProjectActionButton
project={projectAsset}
appRunner={appRunner}
shouldOpenImmediately={nameOfProjectToImmediatelyOpen === projectAsset.title}
shouldCancelOpeningImmediately={shouldCancelOpeningImmediately}
event={projectEvent}
doRefresh={doRefresh}
onOpen={projectBeingOpened => {
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)}
/>
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends ProjectEventType> {
type: Type
}

/** Requests the specified project to be opened. */
export interface ProjectOpenEvent extends ProjectBaseEvent<ProjectEventType.open> {
// 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<ProjectEventType.cancelOpeningAll> {}

/** 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',
Expand Down Expand Up @@ -53,35 +80,23 @@ const SPINNER_CSS_CLASSES: Record<SpinnerState, string> = {
export interface ProjectActionButtonProps {
project: backendModule.Asset<backendModule.AssetType.project>
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<backendModule.AssetType.project>) => void
event: ProjectEvent | null
/** Called when the project is opened via the {@link ProjectActionButton}. */
doOpenManually: () => void
onClose: () => void
openIde: () => void
doRefresh: () => void
}

/** 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<backendModule.ProjectState | null>(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<string | null>(null)

Expand All @@ -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) {
Expand All @@ -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])

Expand Down Expand Up @@ -251,7 +254,6 @@ function ProjectActionButton(props: ProjectActionButtonProps) {

const closeProject = () => {
onClose()
console.log(project.title, 'closing...')
setShouldOpenWhenReady(false)
setState(backendModule.ProjectState.closed)
appRunner?.stopApp()
Expand All @@ -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))
Expand All @@ -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
Expand All @@ -297,16 +290,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
case backendModule.ProjectState.created:
case backendModule.ProjectState.new:
case backendModule.ProjectState.closed:
return (
<button
onClick={async () => {
setShouldOpenWhenReady(true)
await openProject()
}}
>
{svg.PLAY_ICON}
</button>
)
return <button onClick={doOpenManually}>{svg.PLAY_ICON}</button>
case backendModule.ProjectState.openInProgress:
return (
<button onClick={closeProject}>
Expand Down
Loading

0 comments on commit ae7b693

Please sign in to comment.