diff --git a/app/ide-desktop/eslint.config.js b/app/ide-desktop/eslint.config.js
index c4c4d2c70a9f..f3453bc427a3 100644
--- a/app/ide-desktop/eslint.config.js
+++ b/app/ide-desktop/eslint.config.js
@@ -269,6 +269,7 @@ export default [
},
],
'@typescript-eslint/no-confusing-void-expression': 'error',
+ '@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-invalid-void-type': ['error', { allowAsThisParameter: true }],
// React 17 and later supports async functions as event handlers, so we need to disable this
diff --git a/app/ide-desktop/lib/client/src/authentication.ts b/app/ide-desktop/lib/client/src/authentication.ts
index 59b49c2d21ea..2e83ff6dd8da 100644
--- a/app/ide-desktop/lib/client/src/authentication.ts
+++ b/app/ide-desktop/lib/client/src/authentication.ts
@@ -75,9 +75,6 @@
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
-import opener from 'opener'
-
-import * as electron from 'electron'
import * as electron from 'electron'
import opener from 'opener'
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/forgotPassword.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/forgotPassword.tsx
index 424076f51ecc..077e7301310e 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/forgotPassword.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/forgotPassword.tsx
@@ -6,6 +6,7 @@ import * as router from 'react-router-dom'
import * as app from '../../components/app'
import * as auth from '../providers/auth'
import * as svg from '../../components/svg'
+
import Input from './input'
import SvgIcon from './svgIcon'
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/login.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/login.tsx
index cf69cca78f53..734b6e06af48 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/login.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/login.tsx
@@ -7,6 +7,7 @@ import * as fontawesomeIcons from '@fortawesome/free-brands-svg-icons'
import * as app from '../../components/app'
import * as auth from '../providers/auth'
import * as svg from '../../components/svg'
+
import FontAwesomeIcon from './fontAwesomeIcon'
import Input from './input'
import SvgIcon from './svgIcon'
@@ -15,9 +16,6 @@ import SvgIcon from './svgIcon'
// === Constants ===
// =================
-const BUTTON_CLASS_NAME =
- 'relative mt-6 border rounded-md py-2 text-sm text-gray-800 bg-gray-100 hover:bg-gray-200'
-
const LOGIN_QUERY_PARAMS = {
email: 'email',
} as const
@@ -51,7 +49,7 @@ function Login() {
event.preventDefault()
await signInWithGoogle()
}}
- className={BUTTON_CLASS_NAME}
+ className="relative mt-6 border rounded-md py-2 text-sm text-gray-800 bg-gray-100 hover:bg-gray-200"
>
Login with Google
@@ -61,7 +59,7 @@ function Login() {
event.preventDefault()
await signInWithGitHub()
}}
- className={BUTTON_CLASS_NAME}
+ className="relative mt-6 border rounded-md py-2 text-sm text-gray-800 bg-gray-100 hover:bg-gray-200"
>
Login with Github
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/session.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/session.tsx
index be1fbd181bb2..516f38dbe232 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/session.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/session.tsx
@@ -48,31 +48,12 @@ interface SessionProviderProps {
export function SessionProvider(props: SessionProviderProps) {
const { mainPageUrl, children, userSession, registerAuthEventListener } = props
+ const [refresh, doRefresh] = hooks.useRefresh()
+
/** Flag used to avoid rendering child components until we've fetched the user's session at least
* once. Avoids flash of the login screen when the user is already logged in. */
const [initialized, setInitialized] = react.useState(false)
- /** Produces a new object every time.
- * This is not equal to any other empty object because objects are compared by reference.
- * Because it is not equal to the old value, React re-renders the component. */
- function newRefresh() {
- return {}
- }
-
- /** State that, when set, forces a refresh of the user session. This is useful when a
- * user has just logged in (so their cached credentials are out of date). Should be used via the
- * `refreshSession` function. */
- const [refresh, setRefresh] = react.useState(newRefresh())
-
- /** Forces a refresh of the user session.
- *
- * Should be called after any operation that **will** (not **might**) change the user's session.
- * For example, this should be called after signing out. Calling this will result in a re-render
- * of the whole page, which is why it should only be done when necessary. */
- const refreshSession = () => {
- setRefresh(newRefresh())
- }
-
/** Register an async effect that will fetch the user's session whenever the `refresh` state is
* incremented. This is useful when a user has just logged in (as their cached credentials are
* out of date, so this will update them). */
@@ -83,7 +64,7 @@ export function SessionProvider(props: SessionProviderProps) {
setInitialized(true)
return innerSession
},
- [refresh, userSession]
+ [userSession, refresh]
)
/** Register an effect that will listen for authentication events. When the event occurs, we
@@ -97,7 +78,7 @@ export function SessionProvider(props: SessionProviderProps) {
switch (event) {
case listen.AuthEvent.signIn:
case listen.AuthEvent.signOut: {
- refreshSession()
+ doRefresh()
break
}
case listen.AuthEvent.customOAuthState:
@@ -110,7 +91,7 @@ export function SessionProvider(props: SessionProviderProps) {
* See:
* https://github.com/aws-amplify/amplify-js/issues/3391#issuecomment-756473970 */
window.history.replaceState({}, '', mainPageUrl)
- refreshSession()
+ doRefresh()
break
}
default: {
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
index 7a62b529d5b2..c1ecda9868db 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
@@ -101,7 +101,7 @@ export const SECRET_ICON = (
)
@@ -164,6 +164,17 @@ export const ARROW_UP_ICON = (
)
+/** `+`-shaped icon representing creation of an item. */
+export const ADD_ICON = (
+
+)
+
/** An icon representing creation of an item. */
export const CIRCLED_PLUS_ICON = (
-
+
)
@@ -213,6 +224,17 @@ export const SPEECH_BUBBLE_ICON = (
)
+/** `x`-shaped icon representing the closing of a window. */
+export const CLOSE_ICON = (
+
+)
+
// ===========
// === Svg ===
// ===========
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/confirmDeleteModal.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/confirmDeleteModal.tsx
new file mode 100644
index 000000000000..02a0140f444a
--- /dev/null
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/confirmDeleteModal.tsx
@@ -0,0 +1,62 @@
+/** @file Modal for confirming delete of any type of asset. */
+import toast from 'react-hot-toast'
+
+import * as modalProvider from '../../providers/modal'
+import * as svg from '../../components/svg'
+
+import Modal from './modal'
+
+// =================
+// === Component ===
+// =================
+
+export interface ConfirmDeleteModalProps {
+ assetType: string
+ name: string
+ doDelete: () => Promise
+ onSuccess: () => void
+}
+
+function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
+ const { assetType, name, doDelete, onSuccess } = props
+ const { unsetModal } = modalProvider.useSetModal()
+ return (
+
+
+
+ )
+}
+
+export default ConfirmDeleteModal
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/contextMenu.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/contextMenu.tsx
new file mode 100644
index 000000000000..eb5855bfcf93
--- /dev/null
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/contextMenu.tsx
@@ -0,0 +1,28 @@
+/** @file A context menu. */
+
+import * as react from 'react'
+
+// =================
+// === Component ===
+// =================
+
+export interface ContextMenuProps {
+ // `left: number` and `top: number` may be more correct,
+ // however passing an event eliminates the chance
+ // of passing the wrong coordinates from the event.
+ event: react.MouseEvent
+}
+
+function ContextMenu(props: react.PropsWithChildren) {
+ const { children, event } = props
+ return (
+
+ {children}
+
+ )
+}
+
+export default ContextMenu
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/contextMenuEntry.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/contextMenuEntry.tsx
new file mode 100644
index 000000000000..26fdc3695c00
--- /dev/null
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/contextMenuEntry.tsx
@@ -0,0 +1,29 @@
+/** @file An entry in a context menu. */
+
+import * as react from 'react'
+
+export interface ContextMenuEntryProps {
+ disabled?: boolean
+ onClick: (event: react.MouseEvent) => void
+}
+
+// This component MUST NOT use `useState` because it is not rendered directly.
+function ContextMenuEntry(props: react.PropsWithChildren) {
+ const { children, disabled, onClick } = props
+ return (
+
+ )
+}
+
+export default ContextMenuEntry
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/createForm.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/createForm.tsx
new file mode 100644
index 000000000000..b99273ee142a
--- /dev/null
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/createForm.tsx
@@ -0,0 +1,58 @@
+/** @file Base form to create an asset.
+ * This should never be used directly, but instead should be wrapped in a component
+ * that creates a specific asset type. */
+
+import * as react from 'react'
+
+import * as modalProvider from '../../providers/modal'
+import * as svg from '../../components/svg'
+
+import Modal from './modal'
+
+/** The props that should also be in the wrapper component. */
+export interface CreateFormPassthroughProps {
+ left: number
+ top: number
+}
+
+/** `CreateFormPassthroughProps`, plus props that should be defined in the wrapper component. */
+export interface CreateFormProps extends CreateFormPassthroughProps, react.PropsWithChildren {
+ title: string
+ onSubmit: (event: react.FormEvent) => Promise
+}
+
+function CreateForm(props: CreateFormProps) {
+ const { title, left, top, children, onSubmit: wrapperOnSubmit } = props
+ const { unsetModal } = modalProvider.useSetModal()
+
+ async function onSubmit(event: react.FormEvent) {
+ event.preventDefault()
+ await wrapperOnSubmit(event)
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default CreateForm
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 80ef19287bbe..9619f1b9b31a 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
@@ -1,26 +1,38 @@
/** @file Main dashboard component, responsible for listing user's projects as well as other
* interactive components. */
import * as react from 'react'
-import * as reactDom from 'react-dom'
import * as projectManagerModule from 'enso-content/src/project_manager'
import * as auth from '../../authentication/providers/auth'
import * as backend from '../service'
+import * as fileInfo from '../../fileInfo'
+import * as hooks from '../../hooks'
import * as loggerProvider from '../../providers/logger'
import * as modalProvider from '../../providers/modal'
import * as newtype from '../../newtype'
import * as platformModule from '../../platform'
import * as svg from '../../components/svg'
+import * as uploadMultipleFiles from '../../uploadMultipleFiles'
-import Label, * as label from './label'
import PermissionDisplay, * as permissionDisplay from './permissionDisplay'
+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'
+import ConfirmDeleteModal from './confirmDeleteModal'
+import RenameModal from './renameModal'
+import UploadFileModal from './uploadFileModal'
+
+import DirectoryCreateForm from './directoryCreateForm'
+import FileCreateForm from './fileCreateForm'
+import ProjectCreateForm from './projectCreateForm'
+import SecretCreateForm from './secretCreateForm'
+
// =============
// === Types ===
// =============
@@ -32,9 +44,15 @@ export enum Tab {
}
enum ColumnDisplayMode {
+ /** Show only columns which are ready for release. */
+ release = 'release',
+ /** Show all columns. */
all = 'all',
+ /** Show only name and metadata. */
compact = 'compact',
+ /** Show only columns relevant to documentation editors. */
docs = 'docs',
+ /** Show only name, metadata, and configuration options. */
settings = 'settings',
}
@@ -51,10 +69,29 @@ enum Column {
ide = 'ide',
}
+/** Values provided to form creation dialogs. */
+export interface CreateFormProps {
+ left: number
+ top: number
+ backend: backend.Backend
+ directoryId: backend.DirectoryId
+ onSuccess: () => void
+}
+
// =================
// === Constants ===
// =================
+/** Enables features which are not ready for release,
+ * and so are intentionally disabled for release builds. */
+// This type annotation is explicit to undo TypeScript narrowing to `false`,
+// which result in errors about unused code.
+// eslint-disable-next-line @typescript-eslint/no-inferrable-types
+const EXPERIMENTAL: boolean = true
+
+/** The `localStorage` key under which the ID of the current directory is stored. */
+const DIRECTORY_STACK_KEY = 'enso-dashboard-directory-stack'
+
/** English names for the name column. */
const ASSET_TYPE_NAME: Record = {
[backend.AssetType.project]: 'Projects',
@@ -63,6 +100,14 @@ const ASSET_TYPE_NAME: Record = {
[backend.AssetType.directory]: 'Folders',
} as const
+/** Forms to create each asset type. */
+const ASSET_TYPE_CREATE_FORM: Record JSX.Element> = {
+ [backend.AssetType.project]: ProjectCreateForm,
+ [backend.AssetType.file]: FileCreateForm,
+ [backend.AssetType.secret]: SecretCreateForm,
+ [backend.AssetType.directory]: DirectoryCreateForm,
+}
+
/** English names for every column except for the name column. */
const COLUMN_NAME: Record, string> = {
[Column.lastModified]: 'Last modified',
@@ -75,8 +120,35 @@ const COLUMN_NAME: Record, string> = {
[Column.ide]: 'IDE',
} as const
+/** The corresponding `Permissions` for each backend `PermissionAction`. */
+const PERMISSION: Record = {
+ [backend.PermissionAction.own]: { type: permissionDisplay.Permission.owner },
+ [backend.PermissionAction.execute]: {
+ type: permissionDisplay.Permission.regular,
+ read: false,
+ write: false,
+ docsWrite: false,
+ exec: true,
+ },
+ [backend.PermissionAction.edit]: {
+ type: permissionDisplay.Permission.regular,
+ read: false,
+ write: true,
+ docsWrite: false,
+ exec: false,
+ },
+ [backend.PermissionAction.read]: {
+ type: permissionDisplay.Permission.regular,
+ read: true,
+ write: false,
+ docsWrite: false,
+ exec: false,
+ },
+}
+
/** The list of columns displayed on each `ColumnDisplayMode`. */
const COLUMNS_FOR: Record = {
+ [ColumnDisplayMode.release]: [Column.name, Column.lastModified, Column.sharedWith],
[ColumnDisplayMode.all]: [
Column.name,
Column.lastModified,
@@ -104,64 +176,10 @@ const COLUMNS_FOR: Record = {
],
}
-/** React components for every column except for the name column. */
-const COLUMN_RENDERER: Record<
- Exclude,
- (project: backend.Asset) => JSX.Element
-> = {
- [Column.lastModified]: () => <>aa>,
- [Column.sharedWith]: () => <>aa>,
- [Column.docs]: () => <>aa>,
- [Column.labels]: () => (
- <>
-
-
-
- >
- ),
- [Column.dataAccess]: () => (
- <>
-
- ./user_data
-
-
- this folder
-
-
- no access
-
- >
- ),
- [Column.usagePlan]: () => <>aa>,
- [Column.engine]: () => <>aa>,
- [Column.ide]: () => <>aa>,
-}
-
// ========================
// === Helper functions ===
// ========================
-/** English names for every column. */
-function columnName(column: Column, assetType: backend.AssetType) {
- return column === Column.name ? ASSET_TYPE_NAME[assetType] : COLUMN_NAME[column]
-}
-
/** Returns the id of the root directory for a user or organization. */
function rootDirectoryId(userOrOrganizationId: backend.UserOrOrganizationId) {
return newtype.asNewtype(
@@ -169,16 +187,6 @@ function rootDirectoryId(userOrOrganizationId: backend.UserOrOrganizationId) {
)
}
-/** Returns the file extension of a file name. */
-function fileExtension(fileName: string) {
- return fileName.match(/\.(.+?)$/)?.[1] ?? ''
-}
-
-/** Returns the appropriate icon for a specific file extension. */
-function fileIcon(_extension: string) {
- return svg.FILE_ICON
-}
-
// =================
// === Dashboard ===
// =================
@@ -199,46 +207,135 @@ interface OtherDashboardProps extends BaseDashboardProps {
export type DashboardProps = DesktopDashboardProps | OtherDashboardProps
+// TODO[sb]: Implement rename when clicking name of a selected row.
+// There is currently no way to tell whether a row is selected from a column.
+
function Dashboard(props: DashboardProps) {
const { logger, platform } = props
const { accessToken, organization } = auth.useFullUserSession()
const backendService = backend.createBackend(accessToken, logger)
-
const { modal } = modalProvider.useModal()
- const { unsetModal } = modalProvider.useSetModal()
+ const { setModal, unsetModal } = modalProvider.useSetModal()
+
+ const [refresh, doRefresh] = hooks.useRefresh()
- const [searchVal, setSearchVal] = react.useState('')
+ const [query, setQuery] = react.useState('')
const [directoryId, setDirectoryId] = react.useState(rootDirectoryId(organization.id))
const [directoryStack, setDirectoryStack] = react.useState<
backend.Asset[]
>([])
- const [columnDisplayMode, setColumnDisplayMode] = react.useState(ColumnDisplayMode.compact)
- const [selectedAssets, setSelectedAssets] = react.useState([])
+ // Defined by the spec as `compact` by default, however it is not ready yet.
+ const [columnDisplayMode, setColumnDisplayMode] = react.useState(ColumnDisplayMode.release)
- const [projectAssets, setProjectAssets] = react.useState<
+ const [projectAssets, setProjectAssetsRaw] = react.useState<
+ backend.Asset[]
+ >([])
+ const [directoryAssets, setDirectoryAssetsRaw] = react.useState<
+ backend.Asset[]
+ >([])
+ const [secretAssets, setSecretAssetsRaw] = react.useState<
+ backend.Asset[]
+ >([])
+ const [fileAssets, setFileAssetsRaw] = react.useState[]>(
+ []
+ )
+ const [visibleProjectAssets, setVisibleProjectAssets] = react.useState<
backend.Asset[]
>([])
- const [directoryAssets, setDirectoryAssets] = react.useState<
+ const [visibleDirectoryAssets, setVisibleDirectoryAssets] = react.useState<
backend.Asset[]
>([])
- const [secretAssets, setSecretAssets] = react.useState<
+ const [visibleSecretAssets, setVisibleSecretAssets] = react.useState<
backend.Asset[]
>([])
- const [fileAssets, setFileAssets] = react.useState[]>([])
+ const [visibleFileAssets, setVisibleFileAssets] = react.useState<
+ backend.Asset[]
+ >([])
const [tab, setTab] = react.useState(Tab.dashboard)
const [project, setProject] = react.useState(null)
+ const [selectedAssets, setSelectedAssets] = react.useState([])
+ const [isFileBeingDragged, setIsFileBeingDragged] = react.useState(false)
+
const directory = directoryStack[directoryStack.length - 1]
const parentDirectory = directoryStack[directoryStack.length - 2]
+ function setProjectAssets(newProjectAssets: backend.Asset[]) {
+ setProjectAssetsRaw(newProjectAssets)
+ setVisibleProjectAssets(newProjectAssets.filter(asset => asset.title.includes(query)))
+ }
+ function setDirectoryAssets(newDirectoryAssets: backend.Asset[]) {
+ setDirectoryAssetsRaw(newDirectoryAssets)
+ setVisibleDirectoryAssets(newDirectoryAssets.filter(asset => asset.title.includes(query)))
+ }
+ function setSecretAssets(newSecretAssets: backend.Asset[]) {
+ setSecretAssetsRaw(newSecretAssets)
+ setVisibleSecretAssets(newSecretAssets.filter(asset => asset.title.includes(query)))
+ }
+ function setFileAssets(newFileAssets: backend.Asset[]) {
+ setFileAssetsRaw(newFileAssets)
+ setVisibleFileAssets(newFileAssets.filter(asset => asset.title.includes(query)))
+ }
+
+ function exitDirectory() {
+ setDirectoryId(parentDirectory?.id ?? rootDirectoryId(organization.id))
+ setDirectoryStack(
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
+ directoryStack.slice(0, -1)
+ )
+ }
+
+ function enterDirectory(directoryAsset: backend.Asset) {
+ setDirectoryId(directoryAsset.id)
+ setDirectoryStack([...directoryStack, directoryAsset])
+ }
+
+ react.useEffect(() => {
+ const cachedDirectoryStackJson = localStorage.getItem(DIRECTORY_STACK_KEY)
+ if (cachedDirectoryStackJson) {
+ // The JSON was inserted by the code below, so it will always have the right type.
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const cachedDirectoryStack: backend.Asset[] =
+ JSON.parse(cachedDirectoryStackJson)
+ setDirectoryStack(cachedDirectoryStack)
+ const cachedDirectoryId = cachedDirectoryStack[cachedDirectoryStack.length - 1]?.id
+ if (cachedDirectoryId) {
+ setDirectoryId(cachedDirectoryId)
+ }
+ }
+ }, [])
+
+ react.useEffect(() => {
+ if (directoryId === rootDirectoryId(organization.id)) {
+ localStorage.removeItem(DIRECTORY_STACK_KEY)
+ } else {
+ localStorage.setItem(DIRECTORY_STACK_KEY, JSON.stringify(directoryStack))
+ }
+ }, [directoryStack])
+
/** React components for the name column. */
const nameRenderers: {
[Type in backend.AssetType]: (asset: backend.Asset) => JSX.Element
} = {
[backend.AssetType.project]: projectAsset => (
-
>
- items={projectAssets}
+ items={visibleProjectAssets}
getKey={proj => proj.id}
placeholder={
- <>
+
You have no project yet. Go ahead and create one using the form
above.
- >
+
}
columns={COLUMNS_FOR[columnDisplayMode].map(column => ({
id: column,
- name: columnName(column, backend.AssetType.project),
+ heading: ColumnHeading(column, backend.AssetType.project),
render: renderer(column, backend.AssetType.project),
}))}
+ onClick={projectAsset => {
+ setSelectedAssets([projectAsset])
+ }}
+ onContextMenu={(projectAsset, event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ function doOpenForEditing() {
+ // FIXME[sb]: Switch to IDE tab
+ // once merged with `show-and-open-workspace` branch.
+ }
+ function doOpenAsFolder() {
+ // FIXME[sb]: Uncomment once backend support
+ // is in place.
+ // The following code does not typecheck
+ // since `ProjectId`s are not `DirectoryId`s.
+ // enterDirectory(projectAsset)
+ }
+ // This is not a React component even though it contains JSX.
+ // eslint-disable-next-line no-restricted-syntax
+ function doRename() {
+ setModal(() => (
+ Promise.resolve()}
+ onSuccess={doRefresh}
+ />
+ ))
+ }
+ // This is not a React component even though it contains JSX.
+ // eslint-disable-next-line no-restricted-syntax
+ function doDelete() {
+ setModal(() => (
+
+ backendService.deleteProject(projectAsset.id)
+ }
+ onSuccess={doRefresh}
+ />
+ ))
+ }
+ setModal(() => (
+
+
+ Open for editing
+
+
+ Open as folder
+
+
+ Rename
+
+
+ Delete
+
+
+ ))
+ }}
/>
-