Skip to content

Commit

Permalink
Filename input in Cloud File Browser for write component. (#12228)
Browse files Browse the repository at this point in the history
  • Loading branch information
farmaazon authored Feb 13, 2025
1 parent 123126f commit 30e1031
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 206 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
- [Added buttons for editing top-level markdown elements in the documentation
panel][12217].
- [Removed `#` from default colum name][12222]
- [Cloud File Browser will display input for file name in components writing to
(new) files.][12228]

[11889]: https://github.com/enso-org/enso/pull/11889
[11836]: https://github.com/enso-org/enso/pull/11836
Expand All @@ -36,6 +38,7 @@
[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
[12228]: https://github.com/enso-org/enso/pull/12228
[12217]: https://github.com/enso-org/enso/pull/12217

#### Enso Standard Library
Expand Down
2 changes: 1 addition & 1 deletion app/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
"@types/node": "^20.11.21",
"lib0": "^0.2.99",
"react": "^18.3.1",
"vitest": "3.0.3"
"vitest": "3.0.5"
}
}
2 changes: 1 addition & 1 deletion app/gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
"vite": "^6.0.9",
"vite-plugin-vue-devtools": "7.6.8",
"vite-plugin-wasm": "^3.4.1",
"vitest": "3.0.3",
"vitest": "3.0.5",
"vue-react-wrapper": "^0.3.1",
"vue-tsc": "^2.2.0",
"yaml": "^2.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,26 @@ import { computed, h } from 'vue'
const props = defineProps(widgetProps(widgetDefinition))
const writeMode = computed(
() => props.input[ArgumentInfoKey]?.info?.reprType.includes(WRITABLE_FILE_TYPE) ?? false,
)
const item: CustomDropdownItem = {
label: 'Choose file from cloud...',
onClick: ({ setActivity, close }) => {
setActivity(
h(FileBrowserWidget, {
onPathSelected: (path: string) => {
props.onUpdate({
portUpdate: { value: Ast.TextLiteral.new(path), origin: props.input.portId },
directInteraction: true,
})
close()
},
}),
computed(() =>
h(FileBrowserWidget, {
writeMode: writeMode.value,
onPathAccepted: (path: string) => {
props.onUpdate({
portUpdate: { value: Ast.TextLiteral.new(path), origin: props.input.portId },
directInteraction: true,
})
close()
},
}),
),
true,
)
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ import { arrayEquals } from '@/util/data/array'
import type { Opt } from '@/util/data/opt'
import { ProjectPath } from '@/util/projectPath'
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
import { ToValue } from '@/util/reactivity'
import { autoUpdate, offset, shift, size, useFloating } from '@floating-ui/vue'
import type { Ref, RendererNode, VNode } from 'vue'
import { computed, proxyRefs, ref, shallowRef, watch } from 'vue'
import { computed, proxyRefs, ref, shallowRef, toValue, watch } from 'vue'
const props = defineProps(widgetProps(widgetDefinition))
const suggestions = useSuggestionDbStore()
Expand All @@ -51,7 +52,8 @@ const editedWidget = ref<string>()
const editedValue = ref<Ast.Owned<Ast.MutableExpression> | string | undefined>()
const isHovered = ref(false)
/** See @{link Actions.setActivity} */
const activity = shallowRef<VNode>()
const activity = shallowRef<ToValue<VNode>>()
const keepActivityAlive = ref(false)
// How much wider a dropdown can be than a port it is attached to, when a long text is present.
// Any text beyond that limit will receive an ellipsis and sliding animation on hover.
Expand Down Expand Up @@ -336,8 +338,9 @@ function toggleDropdownWidget() {
}
const dropdownActions: Actions = {
setActivity: (newActivity) => {
setActivity: (newActivity, keepAlive = false) => {
activity.value = newActivity
keepActivityAlive.value = keepAlive
},
close: dropDownInteraction.end.bind(dropDownInteraction),
}
Expand Down Expand Up @@ -465,8 +468,11 @@ export interface Actions {
*
* For example, the {@link WidgetCloudBrowser} installs a custom entry that, when clicked,
* opens a file browser where the dropdown was.
* @param keepAlive - when set, the `activity` instance will be kept between drop-down closing
* and opening. The activity component must not change it type (when being a ref) and provide
* `name` option explicitly.
*/
setActivity: (activity: VNode) => void
setActivity: (activity: ToValue<VNode>, keepAlive?: boolean) => void
close: () => void
}
Expand Down Expand Up @@ -513,9 +519,15 @@ declare module '@/providers/widgetRegistry' {
:style="activityStyles"
>
<SizeTransition height :duration="100">
<div v-if="dropDownInteraction.isActive() && activity">
<component :is="activity" />
</div>
<KeepAlive include="KeepAlive">
<KeepAlive v-if="keepActivityAlive">
<component :is="dropDownInteraction.isActive() && activity && toValue(activity)" />
</KeepAlive>
<comopnent
:is="dropDownInteraction.isActive() && activity && toValue(activity)"
v-else
/>
</KeepAlive>
</SizeTransition>
</div>
</Teleport>
Expand Down
137 changes: 93 additions & 44 deletions app/gui/src/project-view/components/widgets/FileBrowserWidget.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
<script lang="ts">
export default {
name: 'FileBrowserWidget',
}
</script>

<script setup lang="ts">
import LoadingSpinner from '@/components/shared/LoadingSpinner.vue'
import SvgButton from '@/components/SvgButton.vue'
Expand All @@ -8,28 +14,29 @@ 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, {
assetIsDatalink,
assetIsDirectory,
assetIsFile,
} from 'enso-common/src/services/Backend'
import { computed, ref, toValue, watch } from 'vue'
import { computed, onMounted, ref, toValue, watch } from 'vue'
import { Err, Ok, Result } from 'ydoc-shared/util/data/result'
const { writeMode = false } = defineProps<{ writeMode?: boolean }>()
const emit = defineEmits<{
pathSelected: [path: string]
pathAccepted: [path: string]
}>()
const { query, fetch, ensureQueryData } = useBackend('remote')
const { remote: backend } = injectBackend()
const errorToast = useToast.error()
const fileName = ref<string>('')
// === Current Directory ===
Expand Down Expand Up @@ -88,15 +95,6 @@ const files = computed(
)
const isEmpty = computed(() => directories.value?.length === 0 && files.value?.length === 0)
// === Selected File ===
interface File {
id: FileId | DatalinkId
title: string
}
const selectedFile = ref<File>()
// === Prefetching ===
watch(directories, (directories) => {
Expand Down Expand Up @@ -126,31 +124,32 @@ function popTo(index: number) {
}
function chooseFile(file: FileAsset | DatalinkAsset) {
selectedFile.value = file
fileName.value = file.title
if (!writeMode) {
acceptCurrentFile()
}
}
const isBusy = computed(
() =>
isDirectoryStackInitializing.value ||
isPending.value ||
(selectedFile.value && currentUser.isPending.value),
)
function acceptCurrentFile() {
if (currentFilePath.value) {
emit('pathAccepted', currentFilePath.value)
} else {
return false
}
}
const isBusy = computed(() => isDirectoryStackInitializing.value || isPending.value)
const anyError = computed(() =>
isError.value ? error
: currentUser.isError.value ? currentUser.error
: undefined,
)
const selectedFilePath = computed(
() =>
selectedFile.value && currentPath.value && `${currentPath.value}${selectedFile.value.title}`,
const currentFilePath = computed(
() => fileName.value && currentPath.value && `${currentPath.value}${fileName.value}`,
)
watch(selectedFilePath, (path) => {
if (path) emit('pathSelected', path)
})
// === Initialization ===
async function enterDirByName(name: string, stack: Directory[]): Promise<Result> {
Expand All @@ -165,23 +164,25 @@ async function enterDirByName(name: string, stack: Directory[]): Promise<Result>
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
},
)
onMounted(() => {
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 Down Expand Up @@ -212,6 +213,26 @@ Promise.all([currentUser.promise.value, currentOrganization.promise.value]).then
</div>
</TransitionGroup>
</div>
<div v-if="writeMode" class="fileNameBar">
<input
v-model="fileName"
class="fileNameInput"
@pointerdown.stop
@click.stop
@contextmenu.stop
@keydown.backspace.stop
@keydown.delete.stop
@keydown.arrow-left.stop
@keydown.arrow-right.stop
@keydown.enter.stop="acceptCurrentFile()"
/>
<SvgButton
class="fileNameAcceptButton"
label="Ok"
:disabled="!fileName"
@click.stop="acceptCurrentFile"
/>
</div>
</div>
</template>

Expand Down Expand Up @@ -286,4 +307,32 @@ Promise.all([currentUser.promise.value, currentOrganization.promise.value]).then
.list-leave-active {
position: absolute;
}
.fileNameBar {
width: 100%;
display: flex;
flex-direction: row;
padding: var(--border-width) 0 0 0;
gap: var(--border-width);
}
.fileNameInput {
border-radius: var(--border-radius-inner);
height: calc(var(--border-radius-inner) * 2);
padding: 0 8px;
background-color: var(--color-frame-selected-bg);
flex-grow: 1;
appearance: textfield;
-moz-appearance: textfield;
user-select: all;
}
.fileNameAcceptButton {
--color-menu-entry-hover-bg: color-mix(in oklab, var(--color-frame-selected-bg), black 10%);
border-radius: var(--border-radius-inner);
height: calc(var(--border-radius-inner) * 2);
margin: 0px;
padding: 4px 12px;
background-color: var(--color-frame-selected-bg);
}
</style>
14 changes: 7 additions & 7 deletions app/gui/src/project-view/providers/interactionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ export class InteractionHandler {
return hasCurrent
}

/** TODO: Add docs */
/**
* Handle pointer event in capture. Calls `pointerdown` handler of currently active handler.
*
* Because usually the handlers check for clicks outside the active panel, even if event is handled,
* it is NOT stopped, and its default action is NOT prevented.
*/
handlePointerEvent<HandlerName extends keyof Interaction>(
event: PointerEvent,
handlerName: Interaction[HandlerName] extends InteractionEventHandler | undefined ? HandlerName
Expand All @@ -77,12 +82,7 @@ export class InteractionHandler {
if (!this.currentInteraction.value) return false
const handler = this.currentInteraction.value[handlerName]
if (!handler) return false
const handled = handler.bind(this.currentInteraction.value)(event) !== false
if (handled) {
event.stopImmediatePropagation()
event.preventDefault()
}
return handled
return handler.bind(this.currentInteraction.value)(event) !== false
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/ydoc-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
"@types/ws": "^8.5.13",
"typescript": "^5.7.2",
"vite-plugin-wasm": "^3.4.1",
"vitest": "3.0.3"
"vitest": "3.0.5"
}
}
2 changes: 1 addition & 1 deletion app/ydoc-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@
"typescript": "^5.7.2",
"vite-node": "3.0.3",
"vite-plugin-wasm": "^3.4.1",
"vitest": "3.0.3"
"vitest": "3.0.5"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.5.14",
"vitest": "3.0.3"
"vitest": "3.0.5"
},
"dependencies": {
"@bazel/bazelisk": "^1.22.1",
Expand Down
Loading

0 comments on commit 30e1031

Please sign in to comment.