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

Filename input in Cloud File Browser for write component. #12228

Merged
merged 9 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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.
Comment on lines +471 to +473
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative approach would be <KeepAlive v-if="..."><component ...></KeepAlive><component v-else ...> (This could be packaged into a ConditionalKeepAlive slotted utility component to avoid writing the <component ....> twice). This would avoid the name requirement and the restriction on changing types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when v-else is hit, the cache will be gone I'm afraid...

...unless we put this in another KeepAlive.

Copy link
Contributor

@kazcw kazcw Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. The reason for the conditional is to disable KeepAlive when the activity hasn't requested it. In that case, losing the cache is intended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagined keepAlive be requested per activity, so for example we have FileBrowser activity which is kept alive, but same drop-down could have another activity which is not: but even when that second activity is set, the FileBrowser is still cached.

*/
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
Loading