Skip to content

Commit

Permalink
Cloud File Browser displays full tree in teams plan. (#12208)
Browse files Browse the repository at this point in the history
Fixes #12002 with one caveat - we are unable to enter projects dirs, so it was not implemented.

https://github.com/user-attachments/assets/f8fbd838-bd43-430d-b0cb-0cc55d5f0bce

I resigned from displaying project, as I think better UX is without them.

# Important Notes
Tested on cloud staging.
  • Loading branch information
farmaazon authored Feb 5, 2025
1 parent fa6b03c commit 2297dca
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 30 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
- [When editing cells or header names in Table Editor Widget, `tab` and `enter`
keys jumps to next cell/ next row respectively.][12129]
- [Fixed bugs occurring after renaming project from within graph editor][12106].
- [Users having "Team" plan or above may now access shared directories in Cloud
File Browser][12208]
- [Added support for rendering numbered and nested lists][12190].
- [Removed `#` from default colum name][12222]

Expand All @@ -29,6 +31,7 @@
[12064]: https://github.com/enso-org/enso/pull/12064
[12129]: https://github.com/enso-org/enso/pull/12129
[12106]: https://github.com/enso-org/enso/pull/12106
[12208]: https://github.com/enso-org/enso/pull/12208
[12190]: https://github.com/enso-org/enso/pull/12190
[12222]: https://github.com/enso-org/enso/pull/12222

Expand Down
15 changes: 15 additions & 0 deletions app/gui/src/project-view/assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,18 @@
--code-editor-default-height: 30%;
--scrollbar-scrollable-opacity: 100%;
}

/* FIXME: Due to some bug in vue, when a component is used both as web components and normally,
It's styles are applied only on the former. Therefore additional "global" definitions must
be added here. */
.LoadingSpinner {
border-radius: 50%;
border: 4px solid;
border-color: rgba(0, 0, 0, 30%) #0000;
animation: s1 0.8s infinite;
}
@keyframes s1 {
to {
transform: rotate(0.5turn);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,8 @@ export const widgetDefinition = defineWidget(
position: absolute;
}
.LoadingSpinner {
border: 4px solid;
border-radius: 100%;
border-color: rgba(255, 255, 255, 90%) #0000;
animation: s1 0.8s infinite;
}
@keyframes s1 {
to {
transform: rotate(0.5turn);
}
}
.v-enter-active,
.v-leave-active {
Expand Down
103 changes: 81 additions & 22 deletions app/gui/src/project-view/components/widgets/FileBrowserWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,56 @@ import LoadingSpinner from '@/components/shared/LoadingSpinner.vue'
import SvgButton from '@/components/SvgButton.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { useBackend } from '@/composables/backend'
import { injectBackend } from '@/providers/backend'
import type { ToValue } from '@/util/reactivity'
import { useToast } from '@/util/toast'
import type {
DatalinkAsset,
DatalinkId,
DirectoryAsset,
DirectoryId,
FileAsset,
FileId,
} from 'enso-common/src/services/Backend'
import Backend, { assetIsDirectory, assetIsFile } from 'enso-common/src/services/Backend'
import Backend, {
assetIsDatalink,
assetIsDirectory,
assetIsFile,
} from 'enso-common/src/services/Backend'
import { computed, ref, toValue, watch } from 'vue'
import { Err, Ok, Result } from 'ydoc-shared/util/data/result'
const emit = defineEmits<{
pathSelected: [path: string]
}>()
const { query, ensureQueryData } = useBackend('remote')
const { query, fetch, ensureQueryData } = useBackend('remote')
const { remote: backend } = injectBackend()
const errorToast = useToast.error()
// === Current Directory ===
interface Directory {
id: DirectoryId | null
id: DirectoryId
title: string
}
const directoryStack = ref<Directory[]>([
{
id: null,
title: 'Cloud',
},
])
const currentDirectory = computed(() => directoryStack.value[directoryStack.value.length - 1]!)
const currentUser = query('usersMe', [])
const currentPath = computed(
() =>
currentUser.data.value &&
`enso://Users/${currentUser.data.value.name}${Array.from(directoryStack.value.slice(1), (frame) => '/' + frame.title).join()}`,
)
const currentOrganization = query('getOrganization', [])
const directoryStack = ref<Directory[]>([])
const isDirectoryStackInitializing = computed(() => directoryStack.value.length === 0)
const currentDirectory = computed(() => directoryStack.value[directoryStack.value.length - 1])
const currentPath = computed(() => {
if (!currentUser.data.value) return
let root = backend?.rootPath(currentUser.data.value) ?? 'enso://'
if (!root.endsWith('/')) root += '/'
return `${root}${directoryStack.value
.slice(1)
.map((dir) => `${dir.title}/`)
.join('')}`
})
// === Directory Contents ===
Expand All @@ -65,17 +79,19 @@ const { isPending, isError, data, error } = query(
)
const compareTitle = (a: { title: string }, b: { title: string }) => a.title.localeCompare(b.title)
const directories = computed(
() => data.value && data.value.filter<DirectoryAsset>(assetIsDirectory).sort(compareTitle),
() => data.value && data.value.filter((asset) => assetIsDirectory(asset)).sort(compareTitle),
)
const files = computed(
() => data.value && data.value.filter<FileAsset>(assetIsFile).sort(compareTitle),
() =>
data.value &&
data.value.filter((asset) => assetIsFile(asset) || assetIsDatalink(asset)).sort(compareTitle),
)
const isEmpty = computed(() => directories.value?.length === 0 && files.value?.length === 0)
// === Selected File ===
interface File {
id: FileId
id: FileId | DatalinkId
title: string
}
Expand All @@ -97,16 +113,27 @@ function enterDir(dir: DirectoryAsset) {
directoryStack.value.push(dir)
}
class DirNotFoundError {
constructor(public dirName: string) {}
toString() {
return `Directory "${this.dirName}" not found`
}
}
function popTo(index: number) {
directoryStack.value.splice(index + 1)
}
function chooseFile(file: FileAsset) {
function chooseFile(file: FileAsset | DatalinkAsset) {
selectedFile.value = file
}
const isBusy = computed(
() => isPending.value || (selectedFile.value && currentUser.isPending.value),
() =>
isDirectoryStackInitializing.value ||
isPending.value ||
(selectedFile.value && currentUser.isPending.value),
)
const anyError = computed(() =>
Expand All @@ -117,12 +144,44 @@ const anyError = computed(() =>
const selectedFilePath = computed(
() =>
selectedFile.value && currentPath.value && `${currentPath.value}/${selectedFile.value.title}`,
selectedFile.value && currentPath.value && `${currentPath.value}${selectedFile.value.title}`,
)
watch(selectedFilePath, (path) => {
if (path) emit('pathSelected', path)
})
// === Initialization ===
async function enterDirByName(name: string, stack: Directory[]): Promise<Result> {
const currentDir = stack[stack.length - 1]
if (currentDir == null) return Err('Stack is empty')
const content = await fetch('listDirectory', listDirectoryArgs(currentDir))
const nextDir = content.find(
(asset): asset is DirectoryAsset => assetIsDirectory(asset) && asset.title === name,
)
if (!nextDir) return Err(new DirNotFoundError(name))
stack.push(nextDir)
return Ok()
}
Promise.all([currentUser.promise.value, currentOrganization.promise.value]).then(
async ([user, organization]) => {
if (!user) {
errorToast.show('Cannot load file list: not logged in.')
return
}
const rootDirectoryId =
backend?.rootDirectoryId(user, organization, null) ?? user.rootDirectoryId
const stack = [{ id: rootDirectoryId, title: 'Cloud' }]
if (rootDirectoryId != user.rootDirectoryId) {
let result = await enterDirByName('Users', stack)
result = result.ok ? await enterDirByName(user.name, stack) : result
if (!result.ok) errorToast.reportError(result.error, 'Cannot enter home directory')
}
directoryStack.value = stack
},
)
</script>

<template>
Expand All @@ -143,7 +202,7 @@ watch(selectedFilePath, (path) => {
<div v-if="isBusy" class="centerContent contents"><LoadingSpinner /></div>
<div v-else-if="anyError" class="centerContent contents">Error: {{ anyError }}</div>
<div v-else-if="isEmpty" class="centerContent contents">Directory is empty</div>
<div v-else :key="currentDirectory.id ?? 'root'" class="listing contents">
<div v-else :key="currentDirectory?.id ?? 'root'" class="listing contents">
<TransitionGroup>
<div v-for="entry in directories" :key="entry.id">
<SvgButton :label="entry.title" name="folder" class="entry" @click="enterDir(entry)" />
Expand Down
9 changes: 8 additions & 1 deletion app/gui/src/project-view/composables/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export function useBackend(which: 'remote' | 'project') {
return useQuery(backendQueryOptions(method, args, backend))
}

function fetch<Method extends BackendMethods>(
method: Method,
args: ToValue<Parameters<Backend[Method]> | undefined>,
): Promise<Awaited<ReturnType<Backend[Method]>>> {
return queryClient.fetchQuery(backendQueryOptions(method, args, backend))
}

/** Enable prefetching of the specified query. */
function prefetch<Method extends BackendMethods>(
method: Method,
Expand All @@ -67,5 +74,5 @@ export function useBackend(which: 'remote' | 'project') {
return queryClient.ensureQueryData(backendQueryOptions(method, args, backend))
}

return { query, prefetch, ensureQueryData }
return { query, fetch, prefetch, ensureQueryData }
}

0 comments on commit 2297dca

Please sign in to comment.