Minor dashboard fixes (#8732)

- Use `rootDirectoryId` returned by backend instead of mirroring backend behavior to generate the root directory ID
- To test this, move/copy an asset *to* the root directory.
- Move the right side of the top bar back to the right edge in editor view (oops)
- To test this one just open any project and make sure the top bar doesn't look funny.
- Delete the barrel export of `#/hooks` in the dashboard (left over due to an oversight when removing the other barrel exports)
- Should not need to be tested; all imports have simply been moved to point to the actual declaring file. As such, as long as the code still typechecks, it should be working fine.
- Delete a duplicated (unused) file caused by a bad merge.

# Important Notes
None
This commit is contained in:
somebody1234 2024-01-11 21:12:59 +10:00 committed by GitHub
parent ec2de192ce
commit b52c81f7e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 100 additions and 295 deletions

View File

@ -42,7 +42,7 @@ import * as detect from 'enso-common/src/detect'
import * as appUtils from '#/appUtils'
import * as authServiceModule from '#/authentication/service'
import * as hooks from '#/hooks'
import * as navigateHooks from '#/hooks/navigateHooks'
import ConfirmRegistration from '#/pages/authentication/ConfirmRegistration'
import EnterOfflineMode from '#/pages/authentication/EnterOfflineMode'
import ForgotPassword from '#/pages/authentication/ForgotPassword'
@ -138,7 +138,7 @@ export default function App(props: AppProps) {
function AppRouter(props: AppProps) {
const { logger, supportsLocalBackend, isAuthenticationDisabled, shouldShowDashboard } = props
const { onAuthenticated, projectManagerUrl } = props
const navigate = hooks.useNavigate()
const navigate = navigateHooks.useNavigate()
if (detect.IS_DEV_MODE) {
// @ts-expect-error This is used exclusively for debugging.
window.navigate = navigate

View File

@ -5,7 +5,8 @@ import BlankIcon from 'enso-assets/blank.svg'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import AssetContextMenu from '#/layouts/dashboard/AssetContextMenu'
import type * as assetsTable from '#/layouts/dashboard/AssetsTable'
import * as authProvider from '#/providers/AuthProvider'
@ -62,7 +63,7 @@ export default function AssetRow(props: AssetRowProps) {
const { organization, user } = authProvider.useNonPartialUserSession()
const { backend } = backendProvider.useBackend()
const { setModal, unsetModal } = modalProvider.useSetModal()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const [isDraggedOver, setIsDraggedOver] = React.useState(false)
const [item, setItem] = React.useState(rawItem)
const dragOverTimeoutHandle = React.useRef<number | null>(null)
@ -102,7 +103,7 @@ export default function AssetRow(props: AssetRowProps) {
)
const copiedAsset = await backend.copyAsset(
asset.id,
newParentId ?? backend.rootDirectoryId(organization),
newParentId ?? organization?.rootDirectoryId ?? backendModule.DirectoryId(''),
asset.title,
null
)
@ -138,7 +139,7 @@ export default function AssetRow(props: AssetRowProps) {
newParentKey: backendModule.AssetId | null,
newParentId: backendModule.DirectoryId | null
) => {
const rootDirectoryId = backend.rootDirectoryId(organization)
const rootDirectoryId = organization?.rootDirectoryId ?? backendModule.DirectoryId('')
const nonNullNewParentKey = newParentKey ?? rootDirectoryId
const nonNullNewParentId = newParentId ?? rootDirectoryId
try {
@ -157,10 +158,7 @@ export default function AssetRow(props: AssetRowProps) {
)
await backend.updateAsset(
asset.id,
{
parentDirectoryId: newParentId ?? backend.rootDirectoryId(organization),
description: null,
},
{ parentDirectoryId: newParentId ?? rootDirectoryId, description: null },
asset.title
)
} catch (error) {
@ -269,7 +267,7 @@ export default function AssetRow(props: AssetRowProps) {
/* should never change */ toastAndLog,
])
hooks.useEventHandler(assetEvents, async event => {
eventHooks.useEventHandler(assetEvents, async event => {
switch (event.type) {
// These events are handled in the specific `NameColumn` files.
case AssetEventType.newProject:

View File

@ -6,7 +6,8 @@ import TriangleDownIcon from 'enso-assets/triangle_down.svg'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as backendProvider from '#/providers/BackendProvider'
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
import * as backendModule from '#/services/backend'
@ -35,7 +36,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
const { item, setItem, selected, setSelected, state, rowState, setRowState } = props
const { numberOfSelectedItems, assetEvents, dispatchAssetListEvent, nodeMap } = state
const { doToggleDirectoryExpansion } = state
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const { backend } = backendProvider.useBackend()
const { shortcuts } = shortcutsProvider.useShortcuts()
const asset = item.item
@ -57,7 +58,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
}
}
hooks.useEventHandler(assetEvents, async event => {
eventHooks.useEventHandler(assetEvents, async event => {
switch (event.type) {
case AssetEventType.newProject:
case AssetEventType.uploadFiles:

View File

@ -3,7 +3,8 @@ import * as React from 'react'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as backendProvider from '#/providers/BackendProvider'
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
import * as backendModule from '#/services/backend'
@ -32,7 +33,7 @@ export interface FileNameColumnProps extends column.AssetColumnProps {}
export default function FileNameColumn(props: FileNameColumnProps) {
const { item, setItem, selected, state, rowState, setRowState } = props
const { assetEvents, dispatchAssetListEvent } = state
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const { backend } = backendProvider.useBackend()
const { shortcuts } = shortcutsProvider.useShortcuts()
const asset = item.item
@ -49,7 +50,7 @@ export default function FileNameColumn(props: FileNameColumnProps) {
return await Promise.resolve(null)
}
hooks.useEventHandler(assetEvents, async event => {
eventHooks.useEventHandler(assetEvents, async event => {
switch (event.type) {
case AssetEventType.newProject:
case AssetEventType.newFolder:

View File

@ -9,7 +9,8 @@ import StopIcon from 'enso-assets/stop.svg'
import type * as assetEvent from '#/events/assetEvent'
import AssetEventType from '#/events/AssetEventType'
import * as hooks from '#/hooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as authProvider from '#/providers/AuthProvider'
import * as backendProvider from '#/providers/BackendProvider'
import * as localStorageProvider from '#/providers/LocalStorageProvider'
@ -81,7 +82,7 @@ export default function ProjectIcon(props: ProjectIconProps) {
const { organization } = authProvider.useNonPartialUserSession()
const { unsetModal } = modalProvider.useSetModal()
const { localStorage } = localStorageProvider.useLocalStorage()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const state = item.projectState.type
const setState = React.useCallback(
(stateOrUpdater: React.SetStateAction<backendModule.ProjectState>) => {
@ -234,7 +235,7 @@ export default function ProjectIcon(props: ProjectIconProps) {
}
}, [onSpinnerStateChange])
hooks.useEventHandler(assetEvents, event => {
eventHooks.useEventHandler(assetEvents, event => {
switch (event.type) {
case AssetEventType.newFolder:
case AssetEventType.uploadFiles:

View File

@ -5,7 +5,8 @@ import NetworkIcon from 'enso-assets/network.svg'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as authProvider from '#/providers/AuthProvider'
import * as backendProvider from '#/providers/BackendProvider'
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
@ -39,7 +40,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
const { item, setItem, selected, rowState, setRowState, state } = props
const { numberOfSelectedItems, assetEvents, dispatchAssetEvent, dispatchAssetListEvent } = state
const { nodeMap, doOpenManually, doOpenIde, doCloseIde } = state
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const { backend } = backendProvider.useBackend()
const { organization } = authProvider.useNonPartialUserSession()
const { shortcuts } = shortcutsProvider.useShortcuts()
@ -86,7 +87,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
}
}
hooks.useEventHandler(assetEvents, async event => {
eventHooks.useEventHandler(assetEvents, async event => {
switch (event.type) {
case AssetEventType.newFolder:
case AssetEventType.newDataConnector:

View File

@ -5,7 +5,8 @@ import ConnectorIcon from 'enso-assets/connector.svg'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import UpsertSecretModal from '#/layouts/dashboard/UpsertSecretModal'
import * as backendProvider from '#/providers/BackendProvider'
import * as modalProvider from '#/providers/ModalProvider'
@ -34,7 +35,7 @@ export interface SecretNameColumnProps extends column.AssetColumnProps {}
export default function SecretNameColumn(props: SecretNameColumnProps) {
const { item, setItem, selected, state, rowState, setRowState } = props
const { assetEvents, dispatchAssetListEvent } = state
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const { setModal } = modalProvider.useSetModal()
const { backend } = backendProvider.useBackend()
const { shortcuts } = shortcutsProvider.useShortcuts()
@ -52,7 +53,7 @@ export default function SecretNameColumn(props: SecretNameColumnProps) {
await Promise.resolve(null)
}
hooks.useEventHandler(assetEvents, async event => {
eventHooks.useEventHandler(assetEvents, async event => {
switch (event.type) {
case AssetEventType.newProject:
case AssetEventType.newFolder:

View File

@ -1,7 +1,7 @@
/** @file A user and their permissions for a specific asset. */
import * as React from 'react'
import * as hooks from '#/hooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as backendProvider from '#/providers/BackendProvider'
import * as backendModule from '#/services/backend'
import * as object from '#/utilities/object'
@ -24,7 +24,7 @@ export default function UserPermissions(props: UserPermissionsProps) {
const { userPermission: initialUserPermission, setUserPermission: outerSetUserPermission } =
props
const { backend } = backendProvider.useBackend()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const [userPermissions, setUserPermissions] = React.useState(initialUserPermission)
React.useEffect(() => {

View File

@ -3,7 +3,7 @@ import * as React from 'react'
import Plus2Icon from 'enso-assets/plus2.svg'
import * as hooks from '#/hooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import Category from '#/layouts/dashboard/CategorySwitcher/Category'
import ManageLabelsModal from '#/layouts/dashboard/ManageLabelsModal'
import * as authProvider from '#/providers/AuthProvider'
@ -36,7 +36,7 @@ export default function LabelsColumn(props: column.AssetColumnProps) {
const session = authProvider.useNonPartialUserSession()
const { setModal, unsetModal } = modalProvider.useSetModal()
const { backend } = backendProvider.useBackend()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const [isHovered, setIsHovered] = React.useState(false)
const self = asset.permissions?.find(
permission => permission.user.user_email === session.organization?.email

View File

@ -1,7 +0,0 @@
/** @file Barrel exports for all React Hooks, other than debug hooks. */
export * from '#/hooks/asyncEffectHooks'
export * from '#/hooks/eventHooks'
export * from '#/hooks/navigateHooks'
export * from '#/hooks/refreshHooks'
export * from '#/hooks/routerHooks'
export * from '#/hooks/toastAndLogHooks'

View File

@ -1,17 +0,0 @@
/** @file Re-exports of React Router hooks. */
import * as router from 'react-router-dom'
/** Returns the current location object, which represents the current URL in web browsers.
*
* Note: If you're using this it may mean you're doing some of your own "routing" in your app,
* and we'd like to know what your use case is. We may be able to provide something higher-level
* to better suit your needs.
* @see {@link https://reactrouter.com/hooks/use-location}
*/
// This is a function, even though it does not look like one.
// eslint-disable-next-line no-restricted-syntax
export const useLocation = router.useLocation
/** Returns the context (if provided) for the child route at this level of the route hierarchy.
* @see {@link https://reactrouter.com/hooks/use-outlet-context} */
export const useOutletContext = router.useOutletContext

View File

@ -5,7 +5,7 @@ import * as toast from 'react-toastify'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import type * as assetsTable from '#/layouts/dashboard/AssetsTable'
import Category from '#/layouts/dashboard/CategorySwitcher/Category'
import GlobalContextMenu from '#/layouts/dashboard/GlobalContextMenu'
@ -63,7 +63,7 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
const { organization, accessToken } = authProvider.useNonPartialUserSession()
const { setModal, unsetModal } = modalProvider.useSetModal()
const { backend } = backendProvider.useBackend()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const asset = item.item
const self = asset.permissions?.find(
permission => permission.user.user_email === organization?.email

View File

@ -4,7 +4,7 @@ import * as React from 'react'
import PenIcon from 'enso-assets/pen.svg'
import type * as assetEvent from '#/events/assetEvent'
import * as hooks from '#/hooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import type Category from '#/layouts/dashboard/CategorySwitcher/Category'
import type * as pageSwitcher from '#/layouts/dashboard/PageSwitcher'
import UserBar from '#/layouts/dashboard/UserBar'
@ -55,7 +55,7 @@ export default function AssetSettingsPanel(props: AssetSettingsPanelProps) {
const [description, setDescription] = React.useState('')
const { organization } = authProvider.useNonPartialUserSession()
const { backend } = backendProvider.useBackend()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const setItem = React.useCallback(
(valueOrUpdater: React.SetStateAction<assetTreeNode.AssetTreeNode>) => {
innerSetItem(valueOrUpdater)

View File

@ -7,7 +7,9 @@ import type * as assetEvent from '#/events/assetEvent'
import AssetEventType from '#/events/AssetEventType'
import type * as assetListEvent from '#/events/assetListEvent'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as asyncEffectHooks from '#/hooks/asyncEffectHooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import type * as assetSearchBar from '#/layouts/dashboard/assetSearchBar'
import type * as assetSettingsPanel from '#/layouts/dashboard/AssetSettingsPanel'
import Category from '#/layouts/dashboard/CategorySwitcher/Category'
@ -354,7 +356,7 @@ export default function AssetsTable(props: AssetsTableProps) {
const { setModal, unsetModal } = modalProvider.useSetModal()
const { localStorage } = localStorageProvider.useLocalStorage()
const { shortcuts } = shortcutsProvider.useShortcuts()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const [initialized, setInitialized] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(true)
const [extraColumns, setExtraColumns] = React.useState(() => new Set<columnUtils.ExtraColumn>())
@ -367,8 +369,8 @@ export default function AssetsTable(props: AssetsTableProps) {
const [, setQueuedAssetEvents] = React.useState<assetEvent.AssetEvent[]>([])
const [, setNameOfProjectToImmediatelyOpen] = React.useState(initialProjectName)
const rootDirectoryId = React.useMemo(
() => backend.rootDirectoryId(organization),
[backend, organization]
() => organization?.rootDirectoryId ?? backendModule.DirectoryId(''),
[organization]
)
const [assetTree, setAssetTree] = React.useState<assetTreeNode.AssetTreeNode>(() => {
const rootParentDirectoryId = backendModule.DirectoryId('')
@ -901,7 +903,7 @@ export default function AssetsTable(props: AssetsTableProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [backend, category])
hooks.useAsyncEffect(
asyncEffectHooks.useAsyncEffect(
null,
async signal => {
switch (backend.type) {
@ -1278,7 +1280,7 @@ export default function AssetsTable(props: AssetsTableProps) {
[rootDirectoryId]
)
hooks.useEventHandler(assetListEvents, event => {
eventHooks.useEventHandler(assetListEvents, event => {
switch (event.type) {
case AssetListEventType.newFolder: {
const siblings = nodeMapRef.current.get(event.parentKey)?.children ?? []

View File

@ -6,7 +6,7 @@ import * as reactDom from 'react-dom'
import CloseLargeIcon from 'enso-assets/close_large.svg'
import * as appUtils from '#/appUtils'
import * as hooks from '#/hooks'
import * as navigateHooks from '#/hooks/navigateHooks'
import * as chat from '#/layouts/dashboard/Chat'
import * as pageSwitcher from '#/layouts/dashboard/PageSwitcher'
import * as loggerProvider from '#/providers/LoggerProvider'
@ -24,7 +24,7 @@ export interface ChatPlaceholderProps {
export default function ChatPlaceholder(props: ChatPlaceholderProps) {
const { page, isOpen, doClose } = props
const logger = loggerProvider.useLogger()
const navigate = hooks.useNavigate()
const navigate = navigateHooks.useNavigate()
const [right, setTargetRight] = animations.useInterpolateOverTime(
animations.interpolationFunctionEaseInOut,
chat.ANIMATION_DURATION_MS,

View File

@ -8,7 +8,8 @@ import type * as assetEvent from '#/events/assetEvent'
import AssetEventType from '#/events/AssetEventType'
import type * as assetListEvent from '#/events/assetListEvent'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as navigateHooks from '#/hooks/navigateHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import type * as assetSearchBar from '#/layouts/dashboard/assetSearchBar'
import type * as assetSettingsPanel from '#/layouts/dashboard/AssetSettingsPanel'
import AssetsTable from '#/layouts/dashboard/AssetsTable'
@ -77,12 +78,12 @@ export default function Drive(props: DriveProps) {
const { loadingProjectManagerDidFail, isListingRemoteDirectoryWhileOffline } = props
const { isListingLocalDirectoryAndWillFail, isListingRemoteDirectoryAndWillFail } = props
const navigate = hooks.useNavigate()
const navigate = navigateHooks.useNavigate()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const { organization } = authProvider.useNonPartialUserSession()
const { backend } = backendProvider.useBackend()
const { localStorage } = localStorageProvider.useLocalStorage()
const { modalRef } = modalProvider.useModalRef()
const toastAndLog = hooks.useToastAndLog()
const [isFileBeingDragged, setIsFileBeingDragged] = React.useState(false)
const [category, setCategory] = React.useState(
() => localStorage.get(localStorageModule.LocalStorageKey.driveCategory) ?? Category.home
@ -96,8 +97,8 @@ export default function Drive(props: DriveProps) {
[labels]
)
const rootDirectoryId = React.useMemo(
() => backend.rootDirectoryId(organization),
[backend, organization]
() => organization?.rootDirectoryId ?? backendModule.DirectoryId(''),
[organization]
)
const isCloud = backend.type === backendModule.BackendType.remote

View File

@ -1,7 +1,7 @@
/** @file The container that launches the IDE. */
import * as React from 'react'
import * as hooks from '#/hooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as backendModule from '#/services/backend'
import * as load from '#/utilities/load'
@ -39,7 +39,7 @@ export interface EditorProps {
/** The container that launches the IDE. */
export default function Editor(props: EditorProps) {
const { hidden, supportsLocalBackend, projectStartupInfo, appRunner } = props
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const [initialized, setInitialized] = React.useState(supportsLocalBackend)
React.useEffect(() => {

View File

@ -31,8 +31,8 @@ export default function GlobalContextMenu(props: GlobalContextMenuProps) {
const { backend } = backendProvider.useBackend()
const { setModal, unsetModal } = modalProvider.useSetModal()
const rootDirectoryId = React.useMemo(
() => backend.rootDirectoryId(organization),
[backend, organization]
() => organization?.rootDirectoryId ?? backendModule.DirectoryId(''),
[organization]
)
const filesInputRef = React.useRef<HTMLInputElement>(null)
const isCloud = backend.type === backendModule.BackendType.remote

View File

@ -1,7 +1,7 @@
/** @file A modal to select labels for an asset. */
import * as React from 'react'
import * as hooks from '#/hooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as authProvider from '#/providers/AuthProvider'
import * as backendProvider from '#/providers/BackendProvider'
import * as modalProvider from '#/providers/ModalProvider'
@ -39,7 +39,7 @@ export default function ManageLabelsModal<
const { organization } = authProvider.useNonPartialUserSession()
const { backend } = backendProvider.useBackend()
const { unsetModal } = modalProvider.useSetModal()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const [labels, setLabelsRaw] = React.useState(item.labels ?? [])
const [query, setQuery] = React.useState('')
const [color, setColor] = React.useState<backendModule.LChColor | null>(null)

View File

@ -4,7 +4,8 @@ import * as React from 'react'
import * as toast from 'react-toastify'
import isEmail from 'validator/es/lib/isEmail'
import * as hooks from '#/hooks'
import * as asyncEffectHooks from '#/hooks/asyncEffectHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as authProvider from '#/providers/AuthProvider'
import * as backendProvider from '#/providers/BackendProvider'
import * as modalProvider from '#/providers/ModalProvider'
@ -53,7 +54,7 @@ export default function ManagePermissionsModal<
const { organization } = authProvider.useNonPartialUserSession()
const { backend } = backendProvider.useBackend()
const { unsetModal } = modalProvider.useSetModal()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const [permissions, setPermissions] = React.useState(item.permissions ?? [])
const [users, setUsers] = React.useState<backendModule.SimpleUser[]>([])
const [email, setEmail] = React.useState<string | null>(null)
@ -103,7 +104,7 @@ export default function ManagePermissionsModal<
// This MUST be an error, otherwise the hooks below are considered as conditionally called.
throw new Error('Cannot share assets on the local backend.')
} else {
const listedUsers = hooks.useAsyncEffect([], () => backend.listUsers(), [])
const listedUsers = asyncEffectHooks.useAsyncEffect([], () => backend.listUsers(), [])
const allUsers = React.useMemo(
() =>
listedUsers.filter(

View File

@ -56,7 +56,9 @@ export default function TopBar(props: TopBarProps) {
{supportsLocalBackend && page !== pageSwitcher.Page.editor && (
<BackendSwitcher setBackendType={setBackendType} />
)}
{page !== pageSwitcher.Page.editor && (
{page === pageSwitcher.Page.editor ? (
<div className="flex-1" />
) : (
<div className="flex-1 flex flex-wrap justify-around">
<AssetSearchBar
query={query}

View File

@ -4,7 +4,8 @@ import * as React from 'react'
import DefaultUserIcon from 'enso-assets/default_user.svg'
import * as appUtils from '#/appUtils'
import * as hooks from '#/hooks'
import * as navigateHooks from '#/hooks/navigateHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import ChangePasswordModal from '#/layouts/dashboard/ChangePasswordModal'
import * as authProvider from '#/providers/AuthProvider'
import * as modalProvider from '#/providers/ModalProvider'
@ -27,11 +28,11 @@ export interface UserMenuProps {
/** Handling the UserMenuItem click event logic and displaying its content. */
export default function UserMenu(props: UserMenuProps) {
const { supportsLocalBackend, onSignOut } = props
const navigate = hooks.useNavigate()
const navigate = navigateHooks.useNavigate()
const { signOut } = authProvider.useAuth()
const { accessToken, organization } = authProvider.useNonPartialUserSession()
const { setModal, unsetModal } = modalProvider.useSetModal()
const toastAndLog = hooks.useToastAndLog()
const toastAndLog = toastAndLogHooks.useToastAndLog()
// The shape of the JWT payload is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment

View File

@ -1,169 +0,0 @@
/** @file The icon and name of a {@link backendModule.SecretAsset}. */
import * as React from 'react'
import ConnectorIcon from 'enso-assets/connector.svg'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import UpsertSecretModal from '#/layouts/dashboard/UpsertSecretModal'
import * as backendProvider from '#/providers/BackendProvider'
import * as modalProvider from '#/providers/ModalProvider'
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
import * as backendModule from '#/services/backend'
import * as assetTreeNode from '#/utilities/assetTreeNode'
import * as eventModule from '#/utilities/event'
import * as indent from '#/utilities/indent'
import * as object from '#/utilities/object'
import * as shortcutsModule from '#/utilities/shortcuts'
import Visibility from '#/utilities/visibility'
import type * as column from '#/components/dashboard/column'
import EditableSpan from '#/components/EditableSpan'
// =====================
// === ConnectorName ===
// =====================
/** Props for a {@link SecretNameColumn}. */
export interface SecretNameColumnProps extends column.AssetColumnProps {}
/** The icon and name of a {@link backendModule.SecretAsset}.
* @throws {Error} when the asset is not a {@link backendModule.SecretAsset}.
* This should never happen. */
export default function SecretNameColumn(props: SecretNameColumnProps) {
const { item, setItem, selected, state, rowState, setRowState } = props
const { assetEvents, dispatchAssetListEvent } = state
const toastAndLog = hooks.useToastAndLog()
const { setModal } = modalProvider.useSetModal()
const { backend } = backendProvider.useBackend()
const { shortcuts } = shortcutsProvider.useShortcuts()
const asset = item.item
if (asset.type !== backendModule.AssetType.secret) {
// eslint-disable-next-line no-restricted-syntax
throw new Error('`SecretNameColumn` can only display secrets.')
}
const setAsset = assetTreeNode.useSetAsset(asset, setItem)
// TODO[sb]: Wait for backend implementation. `editable` should also be re-enabled, and the
// context menu entry should be re-added.
// Backend implementation is tracked here: https://github.com/enso-org/cloud-v2/issues/505.
const doRename = async () => {
await Promise.resolve(null)
}
hooks.useEventHandler(assetEvents, async event => {
switch (event.type) {
case AssetEventType.newProject:
case AssetEventType.newFolder:
case AssetEventType.uploadFiles:
case AssetEventType.openProject:
case AssetEventType.closeProject:
case AssetEventType.cancelOpeningAllProjects:
case AssetEventType.copy:
case AssetEventType.cut:
case AssetEventType.cancelCut:
case AssetEventType.move:
case AssetEventType.delete:
case AssetEventType.restore:
case AssetEventType.download:
case AssetEventType.downloadSelected:
case AssetEventType.removeSelf:
case AssetEventType.temporarilyAddLabels:
case AssetEventType.temporarilyRemoveLabels:
case AssetEventType.addLabels:
case AssetEventType.removeLabels:
case AssetEventType.deleteLabel: {
// Ignored. These events should all be unrelated to secrets.
// `deleteMultiple`, `restoreMultiple`, `download`,
// and `downloadSelected` are handled by `AssetRow`.
break
}
case AssetEventType.newDataConnector: {
if (item.key === event.placeholderId) {
if (backend.type !== backendModule.BackendType.remote) {
toastAndLog('Data connectors cannot be created on the local backend')
} else {
rowState.setVisibility(Visibility.faded)
try {
const id = await backend.createSecret({
parentDirectoryId: asset.parentId,
name: asset.title,
value: event.value,
})
rowState.setVisibility(Visibility.visible)
setAsset(object.merger({ id }))
} catch (error) {
dispatchAssetListEvent({
type: AssetListEventType.delete,
key: item.key,
})
toastAndLog('Error creating new data connector', error)
}
}
}
break
}
}
})
return (
<div
className={`flex text-left items-center whitespace-nowrap rounded-l-full gap-1 px-1.5 py-1 min-w-max ${indent.indentClass(
item.depth
)}`}
onKeyDown={event => {
if (rowState.isEditingName && event.key === 'Enter') {
event.stopPropagation()
}
}}
onClick={event => {
if (
eventModule.isSingleClick(event) &&
(selected ||
shortcuts.matchesMouseAction(shortcutsModule.MouseAction.editName, event))
) {
setRowState(object.merger({ isEditingName: true }))
} else if (eventModule.isDoubleClick(event)) {
event.stopPropagation()
setModal(
<UpsertSecretModal
id={asset.id}
name={asset.title}
doCreate={async (_name, value) => {
try {
await backend.updateSecret(asset.id, { value }, asset.title)
} catch (error) {
toastAndLog(null, error)
}
}}
/>
)
}
}}
>
<img src={ConnectorIcon} className="m-1" />
<EditableSpan
editable={false}
onSubmit={async newTitle => {
setRowState(object.merger({ isEditingName: false }))
if (newTitle !== asset.title) {
const oldTitle = asset.title
setAsset(object.merger({ title: newTitle }))
try {
await doRename()
} catch {
setAsset(object.merger({ title: oldTitle }))
}
}
}}
onCancel={() => {
setRowState(object.merger({ isEditingName: false }))
}}
className="bg-transparent grow leading-170 h-6 py-px"
>
{asset.title}
</EditableSpan>
</div>
)
}

View File

@ -2,10 +2,11 @@
* email address. */
import * as React from 'react'
import * as router from 'react-router-dom'
import * as toastify from 'react-toastify'
import * as appUtils from '#/appUtils'
import * as hooks from '#/hooks'
import * as navigateHooks from '#/hooks/navigateHooks'
import * as authProvider from '#/providers/AuthProvider'
import * as loggerProvider from '#/providers/LoggerProvider'
@ -27,8 +28,8 @@ const REGISTRATION_QUERY_PARAMS = {
export default function ConfirmRegistration() {
const logger = loggerProvider.useLogger()
const auth = authProvider.useAuth()
const location = hooks.useLocation()
const navigate = hooks.useNavigate()
const location = router.useLocation()
const navigate = navigateHooks.useNavigate()
const { verificationCode, email, redirectUrl } = parseUrlSearchParams(location.search)

View File

@ -2,7 +2,7 @@
import * as React from 'react'
import * as appUtils from '#/appUtils'
import * as hooks from '#/hooks'
import * as navigateHooks from '#/hooks/navigateHooks'
import * as authProvider from '#/providers/AuthProvider'
// ========================
@ -12,7 +12,7 @@ import * as authProvider from '#/providers/AuthProvider'
/** An empty component redirecting users based on the backend response to user registration. */
export default function EnterOfflineMode() {
const { goOffline } = authProvider.useAuth()
const navigate = hooks.useNavigate()
const navigate = navigateHooks.useNavigate()
React.useEffect(() => {
void (async () => {

View File

@ -10,7 +10,7 @@ import GoBackIcon from 'enso-assets/go_back.svg'
import LockIcon from 'enso-assets/lock.svg'
import * as appUtils from '#/appUtils'
import * as hooks from '#/hooks'
import * as navigateHooks from '#/hooks/navigateHooks'
import * as authProvider from '#/providers/AuthProvider'
import * as string from '#/utilities/string'
import * as validation from '#/utilities/validation'
@ -36,7 +36,7 @@ const RESET_PASSWORD_QUERY_PARAMS = {
export default function ResetPassword() {
const { resetPassword } = authProvider.useAuth()
const { search } = router.useLocation()
const navigate = hooks.useNavigate()
const navigate = navigateHooks.useNavigate()
const { verificationCode, email } = parseUrlSearchParams(search)

View File

@ -6,7 +6,7 @@ import type * as assetEvent from '#/events/assetEvent'
import AssetEventType from '#/events/AssetEventType'
import type * as assetListEvent from '#/events/assetListEvent'
import AssetListEventType from '#/events/AssetListEventType'
import * as hooks from '#/hooks'
import * as eventHooks from '#/hooks/eventHooks'
import type * as assetSearchBar from '#/layouts/dashboard/assetSearchBar'
import type * as assetSettingsPanel from '#/layouts/dashboard/AssetSettingsPanel'
import AssetSettingsPanel from '#/layouts/dashboard/AssetSettingsPanel'
@ -78,8 +78,8 @@ export default function Dashboard(props: DashboardProps) {
const [openProjectAbortController, setOpenProjectAbortController] =
React.useState<AbortController | null>(null)
const [assetListEvents, dispatchAssetListEvent] =
hooks.useEvent<assetListEvent.AssetListEvent>()
const [assetEvents, dispatchAssetEvent] = hooks.useEvent<assetEvent.AssetEvent>()
eventHooks.useEvent<assetListEvent.AssetListEvent>()
const [assetEvents, dispatchAssetEvent] = eventHooks.useEvent<assetEvent.AssetEvent>()
const [assetSettingsPanelProps, setAssetSettingsPanelProps] =
React.useState<assetSettingsPanel.AssetSettingsPanelRequiredProps | null>(null)
const [isAssetSettingsPanelVisible, setIsAssetSettingsPanelVisible] = React.useState(
@ -89,8 +89,8 @@ export default function Dashboard(props: DashboardProps) {
)
const [initialProjectName, setInitialProjectName] = React.useState(rawInitialProjectName)
const rootDirectoryId = React.useMemo(
() => backend.rootDirectoryId(session.organization),
[backend, session.organization]
() => session.organization?.rootDirectoryId ?? backendModule.DirectoryId(''),
[session.organization]
)
const isListingLocalDirectoryAndWillFail =
@ -142,7 +142,7 @@ export default function Dashboard(props: DashboardProps) {
if (
currentBackend.type === backendModule.BackendType.remote &&
savedProjectStartupInfo.projectAsset.parentId ===
backend.rootDirectoryId(session.organization)
session.organization.rootDirectoryId
) {
// `projectStartupInfo` is still `null`, so the `editor` page will be empty.
setPage(pageSwitcher.Page.drive)
@ -220,7 +220,7 @@ export default function Dashboard(props: DashboardProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
hooks.useEventHandler(assetEvents, event => {
eventHooks.useEventHandler(assetEvents, event => {
switch (event.type) {
case AssetEventType.openProject: {
openProjectAbortController?.abort()

View File

@ -14,7 +14,6 @@ import * as gtag from 'enso-common/src/gtag'
import * as appUtils from '#/appUtils'
import * as cognitoModule from '#/authentication/cognito'
import type * as authServiceModule from '#/authentication/service'
import * as hooks from '#/hooks'
import LoadingScreen from '#/pages/authentication/LoadingScreen'
import * as backendProvider from '#/providers/BackendProvider'
import * as localStorageProvider from '#/providers/LocalStorageProvider'
@ -692,7 +691,7 @@ export function GuestLayout() {
/** A React context hook returning the user session
* for a user that has not yet completed registration. */
export function usePartialUserSession() {
return hooks.useOutletContext<PartialUserSession>()
return router.useOutletContext<PartialUserSession>()
}
// ================================
@ -701,5 +700,5 @@ export function usePartialUserSession() {
/** A React context hook returning the user session for a user that can perform actions. */
export function useNonPartialUserSession() {
return hooks.useOutletContext<Exclude<UserSession, PartialUserSession>>()
return router.useOutletContext<Exclude<UserSession, PartialUserSession>>()
}

View File

@ -4,7 +4,8 @@ import * as React from 'react'
import type * as cognito from '#/authentication/cognito'
import * as listen from '#/authentication/listen'
import * as hooks from '#/hooks'
import * as asyncEffectHooks from '#/hooks/asyncEffectHooks'
import * as refreshHooks from '#/hooks/refreshHooks'
import * as error from '#/utilities/error'
// ======================
@ -51,7 +52,7 @@ export interface SessionProviderProps {
export default function SessionProvider(props: SessionProviderProps) {
const { mainPageUrl, children, userSession, registerAuthEventListener } = props
const [refresh, doRefresh] = hooks.useRefresh()
const [refresh, doRefresh] = refreshHooks.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. */
@ -60,7 +61,7 @@ export default function SessionProvider(props: SessionProviderProps) {
/** Register an async effect that will fetch the user's session whenever the `refresh` state is
* set. This is useful when a user has just logged in (as their cached credentials are
* out of date, so this will update them). */
const session = hooks.useAsyncEffect(
const session = asyncEffectHooks.useAsyncEffect(
null,
async () => {
const innerSession = await userSession()

View File

@ -93,6 +93,7 @@ export interface UserOrOrganization {
/** If `false`, this account is awaiting acceptance from an admin, and endpoints other than
* `usersMe` will not work. */
isEnabled: boolean
rootDirectoryId: DirectoryId
}
/** A `Directory` returned by `createDirectory`. */
@ -835,8 +836,6 @@ export function stripProjectExtension(name: string) {
export abstract class Backend {
abstract readonly type: BackendType
/** Return the root directory id for the given user. */
abstract rootDirectoryId(user: UserOrOrganization | null): DirectoryId
/** Return a list of all users in the same organization. */
abstract listUsers(): Promise<SimpleUser[]>
/** Set the username of the current user. */

View File

@ -42,10 +42,6 @@ export class LocalBackend extends backend.Backend {
}
}
/** Return the root directory id for the given user. */
override rootDirectoryId(): backend.DirectoryId {
return backend.DirectoryId('')
}
/** Return a list of assets in a directory.
* @throws An error if the JSON-RPC call fails. */
override async listDirectory(): Promise<backend.AnyAsset[]> {

View File

@ -155,20 +155,6 @@ export class RemoteBackend extends backendModule.Backend {
throw new Error(message)
}
/** Return the root directory id for the given user. */
override rootDirectoryId(
user: backendModule.UserOrOrganization | null
): backendModule.DirectoryId {
if (user != null && !user.id.startsWith('organization-')) {
this.logger.error(`User ID '${user.id}' does not start with 'organization-'`)
}
return backendModule.DirectoryId(
// `user` is only null when the user is offline, in which case the remote backend cannot
// be accessed anyway.
user?.id.replace(/^organization-/, `${backendModule.AssetType.directory}-`) ?? ''
)
}
/** Return a list of all users in the same organization. */
override async listUsers(): Promise<backendModule.SimpleUser[]> {
const path = remoteBackendPaths.LIST_USERS_PATH

View File

@ -37,12 +37,13 @@ export async function mockApi(page: test.Page) {
const defaultEmail = 'email@example.com' as backend.EmailAddress
const defaultUsername = 'user name'
const defaultOrganizationId = backend.UserOrOrganizationId('organization-placeholder id')
const defaultDirectoryId = backend.UserOrOrganizationId('directory-placeholder id')
const defaultDirectoryId = backend.DirectoryId('directory-placeholder id')
const defaultUser: backend.UserOrOrganization = {
email: defaultEmail,
name: defaultUsername,
id: defaultOrganizationId,
isEnabled: true,
rootDirectoryId: defaultDirectoryId,
}
let currentUser: backend.UserOrOrganization | null = defaultUser
const assetMap = new Map<backend.AssetId, backend.AnyAsset>()
@ -297,11 +298,16 @@ export async function mockApi(page: test.Page) {
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.CreateUserRequestBody = await request.postDataJSON()
const id = body.organizationId ?? defaultUser.id
const rootDirectoryId = backend.DirectoryId(
id.replace(/^organization-/, 'directory-')
)
currentUser = {
email: body.userEmail,
name: body.userName,
id: body.organizationId ?? defaultUser.id,
id,
isEnabled: false,
rootDirectoryId,
}
await route.fulfill({ json: currentUser })
} else if (request.method() === 'GET') {