Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand parent directories of opened projects #11947

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4c5729f
Expand parent directories of opened projects
somebody1234 Dec 26, 2024
edddae0
Fix expanding local directories
somebody1234 Dec 26, 2024
8269f4e
Automatically expand "Users" or "Teams" root directory as appropriate
somebody1234 Dec 30, 2024
e5259d5
Fix errors
somebody1234 Dec 30, 2024
7b96344
Merge branch 'develop' into wip/sb/expand-parent-directories
somebody1234 Dec 30, 2024
b2b361b
Only expand ancestors of initially launched projects once
somebody1234 Dec 31, 2024
e943fe1
WIP: Separate ancestor lists for each Drive sidebar category
somebody1234 Dec 31, 2024
2cee8ea
Fix errors
somebody1234 Jan 2, 2025
68000a7
Merge branch 'develop' into wip/sb/expand-parent-directories
somebody1234 Jan 3, 2025
364c3af
Fix errors
somebody1234 Jan 3, 2025
1df6478
Fix bugs
somebody1234 Jan 3, 2025
21b3489
Remove obsolete test
somebody1234 Jan 3, 2025
6adda38
Replace `category` with `categoryType` in `listDirectoryQueryOptions`
somebody1234 Jan 6, 2025
eca7931
Move logic for computing ancestors from `Dashboard.tsx` to individual…
somebody1234 Jan 7, 2025
0fa64ce
Extract computing initial expanded directories to a function
somebody1234 Jan 7, 2025
17eb1df
Compute initial expanded directories in initial value store instead o…
somebody1234 Jan 7, 2025
356cfec
Fix cyclic dependencies between stores
somebody1234 Jan 8, 2025
d01fc61
Fix errors
somebody1234 Jan 8, 2025
3091835
Expand full path to directory when clicking on breadcrumb
somebody1234 Jan 8, 2025
8f4032e
Remove `useSetExpandedDirectories`
somebody1234 Jan 8, 2025
58d59ec
Add `useIsDirectoryExpanded`
somebody1234 Jan 8, 2025
6bcfaf7
WIP: Re-add `DriveProvider.test`
somebody1234 Jan 8, 2025
98727bf
WIP: Fix `DriveProvider.test`
somebody1234 Jan 8, 2025
a7f81ae
Fix unnecessary dependencies of `DriveProvider`
somebody1234 Jan 8, 2025
4602cc6
help
somebody1234 Jan 8, 2025
afdc27b
WIP: Fix `DriveProvider.test`
somebody1234 Jan 8, 2025
71a1b8c
Fix `DriveProvider.test`
somebody1234 Jan 8, 2025
ffc6e15
Fix type errors
somebody1234 Jan 8, 2025
42fa910
Merge branch 'develop' into wip/sb/expand-parent-directories
somebody1234 Jan 9, 2025
25a05ca
Fix errors
somebody1234 Jan 9, 2025
571a60b
Fix integration tests
somebody1234 Jan 9, 2025
6b43868
Merge branch 'develop' into wip/sb/expand-parent-directories
somebody1234 Jan 13, 2025
6e6b573
Fix lint error
somebody1234 Jan 13, 2025
41ae083
Minor fixes
somebody1234 Jan 14, 2025
2d1bdbf
Merge branch 'develop' into wip/sb/expand-parent-directories
somebody1234 Jan 16, 2025
736c51e
Merge branch 'develop' into wip/sb/expand-parent-directories
somebody1234 Jan 19, 2025
4c5aef3
Fix integration tests
somebody1234 Jan 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/common/src/services/Backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ export type UserPermissionIdentifier = UserGroupId | UserId
export type Path = newtype.Newtype<string, 'Path'>
export const Path = newtype.newtypeConstructor<Path>()

/** Whether the given path is a descendant of another path. */
export function isDescendantPath(path: Path, possibleAncestor: Path) {
return path.startsWith(`${possibleAncestor}/`)
}

const PLACEHOLDER_USER_GROUP_PREFIX = 'usergroup-placeholder-'

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ export interface DirectoryNameColumnProps extends column.AssetColumnProps {
*/
export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
const { item, depth, selected, state, rowState, setRowState, isEditable } = props
const { backend, nodeMap } = state
const { backend, nodeMap, category } = state
const { getText } = textProvider.useText()
const driveStore = useDriveStore()
const toggleDirectoryExpansion = useToggleDirectoryExpansion()
const isExpanded = useStore(driveStore, (storeState) =>
storeState.expandedDirectoryIds.includes(item.id),
const isExpanded = useStore(
somebody1234 marked this conversation as resolved.
Show resolved Hide resolved
driveStore,
(storeState) => storeState.expandedDirectories[category.rootPath]?.includes(item.id) ?? false,
)

const updateDirectoryMutation = useMutation(backendMutationOptions(backend, 'updateDirectory'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import { useEventCallback } from '#/hooks/eventCallbackHooks'
import { useCategoriesAPI, useCloudCategoryList } from '#/layouts/Drive/Categories/categoriesHooks'
import type { AnyCloudCategory } from '#/layouts/Drive/Categories/Category'
import { useUser } from '#/providers/AuthProvider'
import { useSetExpandedDirectoryIds, useSetSelectedKeys } from '#/providers/DriveProvider'
import {
useDriveStore,
useSetExpandedDirectories,
useSetSelectedKeys,
} from '#/providers/DriveProvider'
import type { DirectoryId } from '#/services/Backend'
import { isDirectoryId } from '#/services/Backend'
import { unsafeMutable } from 'enso-common/src/utilities/data/object'
import { Fragment, useTransition } from 'react'
import invariant from 'tiny-invariant'
import type { AssetColumnProps } from '../column'
Expand All @@ -20,11 +25,12 @@ export default function PathColumn(props: AssetColumnProps) {

const { virtualParentsPath, parentsPath } = item

const { getAssetNodeById } = state
const { getAssetNodeById, category } = state

const driveStore = useDriveStore()
const { setCategory } = useCategoriesAPI()
const setSelectedKeys = useSetSelectedKeys()
const setExpandedDirectoryIds = useSetExpandedDirectoryIds()
const setExpandedDirectories = useSetExpandedDirectories()

// Path navigation exist only for cloud categories.
const { getCategoryByDirectoryId } = useCloudCategoryList()
Expand Down Expand Up @@ -65,11 +71,16 @@ export default function PathColumn(props: AssetColumnProps) {
const targetDirectoryNode = getAssetNodeById(targetDirectory)

if (targetDirectoryNode == null && rootDirectoryInThePath.categoryId != null) {
// We reassign the variable only to make TypeScript happy here.
const categoryId = rootDirectoryInThePath.categoryId

setCategory(categoryId)
setExpandedDirectoryIds(pathToDirectory.map(({ id }) => id).concat(targetDirectory))
setCategory(rootDirectoryInThePath.categoryId)
const expandedDirectories = structuredClone(driveStore.getState().expandedDirectories)
somebody1234 marked this conversation as resolved.
Show resolved Hide resolved
// This is SAFE, as it is a fresh copy that has been deep cloned above.
const directoryList = expandedDirectories[category.rootPath]
if (directoryList) {
unsafeMutable(directoryList).push(
...pathToDirectory.map(({ id }) => id).concat(targetDirectory),
)
}
setExpandedDirectories(expandedDirectories)
}

setSelectedKeys(new Set([targetDirectory]))
Expand Down
9 changes: 5 additions & 4 deletions app/gui/src/dashboard/layouts/Drive/Categories/Category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import { newDirectoryId } from '#/services/LocalBackend'
const PATH_SCHEMA = z.string().refine((s): s is Path => true)
const DIRECTORY_ID_SCHEMA = z.string().refine((s): s is DirectoryId => true)

const EACH_CATEGORY_SCHEMA = z.object({ label: z.string(), icon: z.string() })
const EACH_CATEGORY_SCHEMA = z.object({
label: z.string(),
icon: z.string(),
rootPath: PATH_SCHEMA,
})

/** A category corresponding to the root of the user or organization. */
const CLOUD_CATEGORY_SCHEMA = z
Expand Down Expand Up @@ -61,7 +65,6 @@ export const USER_CATEGORY_SCHEMA = z
type: z.literal('user'),
user: z.custom<User>(() => true),
id: z.custom<UserId>(() => true),
rootPath: PATH_SCHEMA,
homeDirectoryId: DIRECTORY_ID_SCHEMA,
})
.merge(EACH_CATEGORY_SCHEMA)
Expand All @@ -74,7 +77,6 @@ export const TEAM_CATEGORY_SCHEMA = z
type: z.literal('team'),
id: z.custom<UserGroupId>(() => true),
team: z.custom<UserGroupInfo>(() => true),
rootPath: PATH_SCHEMA,
homeDirectoryId: DIRECTORY_ID_SCHEMA,
})
.merge(EACH_CATEGORY_SCHEMA)
Expand All @@ -96,7 +98,6 @@ export const LOCAL_DIRECTORY_CATEGORY_SCHEMA = z
.object({
type: z.literal('local-directory'),
id: z.custom<DirectoryId>(() => true),
rootPath: PATH_SCHEMA,
homeDirectoryId: DIRECTORY_ID_SCHEMA,
})
.merge(EACH_CATEGORY_SCHEMA)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,28 +84,31 @@ export function useCloudCategoryList() {

const hasUserAndTeamSpaces = userHasUserAndTeamSpaces(user)

const homeDirectoryId =
hasUserAndTeamSpaces ? organizationIdToDirectoryId(organizationId) : userIdToDirectoryId(userId)

const cloudCategory: CloudCategory = {
type: 'cloud',
id: 'cloud',
label: getText('cloudCategory'),
icon: CloudIcon,
homeDirectoryId:
hasUserAndTeamSpaces ?
organizationIdToDirectoryId(organizationId)
: userIdToDirectoryId(userId),
rootPath: Path(`enso://${hasUserAndTeamSpaces ? `Users/${user.name}` : ''}`),
homeDirectoryId,
}

const recentCategory: RecentCategory = {
type: 'recent',
id: 'recent',
label: getText('recentCategory'),
rootPath: Path(`(Recent)`),
icon: RecentIcon,
}

const trashCategory: TrashCategory = {
type: 'trash',
id: 'trash',
label: getText('trashCategory'),
rootPath: Path(`(Trash)`),
icon: Trash2Icon,
}

Expand Down Expand Up @@ -219,6 +222,10 @@ export function useLocalCategoryList() {
type: 'local',
id: 'local',
label: getText('localCategory'),
/** The root path of this category. */
get rootPath() {
somebody1234 marked this conversation as resolved.
Show resolved Hide resolved
return localBackend?.rootPath() ?? Path('')
},
icon: ComputerIcon,
}

Expand Down
10 changes: 5 additions & 5 deletions app/gui/src/dashboard/layouts/Drive/directoryIdsHooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Path, createRootDirectoryAsset } from 'enso-common/src/services/Backend
import type { Category } from '#/layouts/CategorySwitcher/Category'
import { useFullUserSession } from '#/providers/AuthProvider'
import { useBackend } from '#/providers/BackendProvider'
import { useExpandedDirectoryIds, useSetExpandedDirectoryIds } from '#/providers/DriveProvider'
import { useExpandedDirectories, useSetExpandedDirectories } from '#/providers/DriveProvider'
import { useLocalStorageState } from '#/providers/LocalStorageProvider'

/** Options for {@link useDirectoryIds}. */
Expand All @@ -35,8 +35,8 @@ export function useDirectoryIds(options: UseDirectoryIdsOptions) {
* The root directory is not included as it might change when a user switches
* between items in sidebar and we don't want to reset the expanded state using `useEffect`.
*/
const privateExpandedDirectoryIds = useExpandedDirectoryIds()
const setExpandedDirectoryIds = useSetExpandedDirectoryIds()
const privateExpandedDirectoryIds = useExpandedDirectories()
const setExpandedDirectoryIds = useSetExpandedDirectories()

const [localRootDirectory] = useLocalStorageState('localRootDirectory')

Expand All @@ -53,8 +53,8 @@ export function useDirectoryIds(options: UseDirectoryIdsOptions) {
const rootDirectory = useMemo(() => createRootDirectoryAsset(rootDirectoryId), [rootDirectoryId])

const expandedDirectoryIds = useMemo(
() => [rootDirectoryId].concat(privateExpandedDirectoryIds),
[privateExpandedDirectoryIds, rootDirectoryId],
() => [rootDirectoryId].concat(privateExpandedDirectoryIds[category.rootPath] ?? []),
[category.rootPath, privateExpandedDirectoryIds, rootDirectoryId],
)

return { setExpandedDirectoryIds, rootDirectoryId, rootDirectory, expandedDirectoryIds } as const
Expand Down
137 changes: 136 additions & 1 deletion app/gui/src/dashboard/pages/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,19 @@ import * as backendModule from '#/services/Backend'
import * as localBackendModule from '#/services/LocalBackend'
import * as projectManager from '#/services/ProjectManager'

import { listDirectoryQueryOptions, useBackendQuery } from '#/hooks/backendHooks'
import { useCategoriesAPI } from '#/layouts/Drive/Categories/categoriesHooks'
import { useDriveStore, useSetExpandedDirectories } from '#/providers/DriveProvider'
import { userGroupIdToDirectoryId, userIdToDirectoryId } from '#/services/RemoteBackend'
import { TEAMS_DIRECTORY_ID, USERS_DIRECTORY_ID } from '#/services/remoteBackendPaths'
import { baseName } from '#/utilities/fileInfo'
import { tryFindSelfPermission } from '#/utilities/permissions'
import { STATIC_QUERY_OPTIONS } from '#/utilities/reactQuery'
import * as sanitizedEventTargets from '#/utilities/sanitizedEventTargets'
import { usePrefetchQuery } from '@tanstack/react-query'
import { usePrefetchQuery, useQueryClient } from '@tanstack/react-query'
import { EMPTY_ARRAY } from 'enso-common/src/utilities/data/array'
import { unsafeEntries, unsafeMutable } from 'enso-common/src/utilities/data/object'
import invariant from 'tiny-invariant'
import { DashboardTabPanels } from './DashboardTabPanels'

// =================
Expand All @@ -76,6 +83,7 @@ export default function Dashboard(props: DashboardProps) {
<CategoriesProvider onCategoryChange={resetAssetTableState}>
<EventListProvider>
<ProjectsProvider>
<OpenedProjectsParentsExpander />
<DashboardInner {...props} />
</ProjectsProvider>
</EventListProvider>
Expand Down Expand Up @@ -308,3 +316,130 @@ function DashboardInner(props: DashboardProps) {
</Page>
)
}

/** Expand the list of parents for opened projects. */
function OpenedProjectsParentsExpander() {
const queryClient = useQueryClient()
const remoteBackend = backendProvider.useRemoteBackend()
const localBackend = backendProvider.useLocalBackend()
const categoriesAPI = useCategoriesAPI()
const { category, cloudCategories, localCategories } = categoriesAPI
const driveStore = useDriveStore()
const launchedProjects = useLaunchedProjects()
const setExpandedDirectories = useSetExpandedDirectories()
const { user } = authProvider.useFullUserSession()
const { data: userGroups } = useBackendQuery(remoteBackend, 'listUserGroups', [])
const { data: users } = useBackendQuery(remoteBackend, 'listUsers', [])

const updateOpenedProjects = eventCallbacks.useEventCallback(async () => {
const userGroupDirectoryIds = new Set(
(user.userGroups ?? EMPTY_ARRAY).map(userGroupIdToDirectoryId),
)

const expandedDirectories = structuredClone(driveStore.getState().expandedDirectories)
for (const otherCategory of [...cloudCategories.categories, ...localCategories.categories]) {
expandedDirectories[otherCategory.rootPath] ??= []
}

if (localBackend) {
const localProjects = launchedProjects.filter(
(project) => project.type === backendModule.BackendType.local,
)
for (const project of localProjects) {
const path = localBackendModule.extractTypeAndId(project.parentId).id
for (const [rootPath, directoriesInCategory] of unsafeEntries(expandedDirectories)) {
const strippedPath = path.replace(`${rootPath}/`, '')
if (strippedPath !== path) {
let parentPath = String(rootPath)
const parents = strippedPath.split('/')
for (const parent of parents) {
parentPath += `/${parent}`
const currentParentPath = backendModule.Path(parentPath)
const currentParentId = localBackendModule.newDirectoryId(currentParentPath)
if (!backendModule.isDescendantPath(currentParentPath, rootPath)) {
continue
}
if (directoriesInCategory.includes(currentParentId)) {
continue
}
const id = localBackendModule.newDirectoryId(currentParentPath)
// This is SAFE as the value has been `structuredClone`d above.
unsafeMutable(directoriesInCategory).push(id)
}
}
}
}
}

const cloudProjects = launchedProjects.filter(
(project) => project.type === backendModule.BackendType.remote,
)
const promises = cloudProjects.map((project) =>
somebody1234 marked this conversation as resolved.
Show resolved Hide resolved
queryClient.ensureQueryData(
listDirectoryQueryOptions({
backend: remoteBackend,
parentId: project.parentId,
category,
}),
),
)
const projectsSiblings = await Promise.allSettled(promises)
const projects = projectsSiblings.flatMap((directoryResult, i) => {
const projectInfo = cloudProjects[i]
const project =
projectInfo && directoryResult.status === 'fulfilled' ?
directoryResult.value
.filter(backendModule.assetIsProject)
.find((asset) => asset.id === projectInfo.id)
: null
return project ? [project] : []
})
for (const project of projects) {
const parents = project.parentsPath.split('/').filter(backendModule.isDirectoryId)
const rootDirectoryId = parents[0]
const baseVirtualPath = (() => {
const userGroupName = userGroups?.find(
(userGroup) => userGroupIdToDirectoryId(userGroup.id) === rootDirectoryId,
)?.groupName
if (userGroupName != null) {
return `enso://Teams/${userGroupName}`
}
const userName = users?.find(
(otherUser) => userIdToDirectoryId(otherUser.userId) === rootDirectoryId,
)?.name
if (userName != null) {
return `enso://Users/${userGroupName}`
}
})()
const virtualPath = backendModule.Path(`${baseVirtualPath}/${project.virtualParentsPath}`)
invariant(
baseVirtualPath != null,
'The root directory must be either a user directory or a team directory.',
)
for (const [categoryRootPath, directoriesInCategoryRaw] of unsafeEntries(
expandedDirectories,
)) {
const directoriesInCategory = unsafeMutable(directoriesInCategoryRaw)
if (!backendModule.isDescendantPath(virtualPath, categoryRootPath)) {
continue
}
if ((rootDirectoryId && userGroupDirectoryIds.has(rootDirectoryId)) ?? false) {
directoriesInCategory.push(TEAMS_DIRECTORY_ID)
} else {
directoriesInCategory.push(USERS_DIRECTORY_ID)
}
for (const parent of parents) {
directoriesInCategory.push(parent)
}
}
}

setExpandedDirectories(expandedDirectories)
})

React.useEffect(() => {
somebody1234 marked this conversation as resolved.
Show resolved Hide resolved
void updateOpenedProjects()
}, [updateOpenedProjects])

return null
}
Loading
Loading