8000 File browser extensions by vitvakatu · Pull Request #13048 · enso-org/enso · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

File browser extensions #13048

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

Merged
merged 33 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
779e7a5
Add file extension text input to file browser widget
vitvakatu Apr 22, 2025
2c0ee04
Fix scrolling of file browser widget
vitvakatu Apr 22, 2025
803d869
Adding SelectionSubmenu dropdown for file extension input
vitvakatu Apr 23, 2025
831c583
Nested interactions
vitvakatu Apr 24, 2025
3d67c81
Filtering file list
vitvakatu Apr 28, 2025
c96e14e
abstract nested selection menu API
vitvakatu May 6, 2025
f402d97
Fix dropdown interaction
vitvakatu May 7, 2025
505a6c9
lib-facing API
vitvakatu May 7, 2025
7410d8f
native file browser
vitvakatu Apr 30, 2025
36da349
fix dynamic config format
vitvakatu May 7, 2025
8847bfb
Config-based extensions list
vitvakatu May 7, 2025
d66a302
Rename fields of selection submenu entries
vitvakatu May 8, 2025
19677ad
converting fileetensionentries
vitvakatu May 8, 2025
03ab33a
Refactorings and fixes
vitvakatu May 14, 2025
a242d0b
Fix typecheck and unit test failure
vitvakatu May 14, 2025
9780077
Update changelog
vitvakatu May 14, 2025
7250674
Fixing behavior of the file extension filter
vitvakatu May 14, 2025
47b21e3
Addressing minor review comments
vitvakatu May 15, 2025
09c8da7
Move FileFilter type to enso-gui
vitvakatu May 15, 2025
52b3028
simplify interaction handler implementation
vitvakatu May 15, 2025
cafcedc
remove unneeded fallthrough attributes
vitvakatu May 15, 2025
f3c368e
wip: Move file name bar to a separate component
vitvakatu May 15, 2025
03879e8
Merge with develop
vitvakatu May 16, 2025
2eb0b51
Merge with develop
vitvakatu May 20, 2025
fa46a50
Fixing remaining conflicts, integrating changes
vitvakatu May 20, 2025
8121827
Avoid the need in nextTick when updating the contents of the input
vitvakatu May 20, 2025
67ab864
Close file browser on accept
vitvakatu May 20, 2025
8bcf335
Correct modal display, fix style for acceptance button
vitvakatu May 20, 2025
1d7a949
Show file extension picker in read-mode file browser, fix empty
vitvakatu May 20, 2025
d42d43e
Remove comments about interactions handling order
vitvakatu May 20, 2025
f877718
regenerate engine docs
vitvakatu May 21, 2025
7105d7b
Merge with develop
vitvakatu May 21, 2025
6e38b1f
Merge with develop
vitvakatu May 22, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Add ability to inspect column, row and value from right click on table
viz][12986]
- [Add option to browse cloud for folders][13117]
- [File Browser Widget: Add ability to filter files by extension][13048]
- [Add keyboard shortcuts for formatting documentation][13134]

[12774]: https://github.com/enso-org/enso/pull/12774
Expand All @@ -26,6 +27,7 @@
[13014]: https://github.com/enso-org/enso/pull/13014
[12986]: https://github.com/enso-org/enso/pull/12986
[13117]: https://github.com/enso-org/enso/pull/13117
[13048]: https://github.com/enso-org/enso/pull/13048
[13134]: https://github.com/enso-org/enso/pull/13134

#### Enso Standard Library
Expand Down
2 changes: 2 additions & 0 deletions app/gui/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/// <reference types="vite/client" />
import type * as saveAccessToken from 'enso-common/src/accessToken'
import type { $Config } from './src/config'
import type { FileFilter } from './src/project-view/util/fileFilter'

/** Nested configuration options with `string` values. */
interface StringConfig {
Expand Down Expand Up @@ -118,6 +119,7 @@ interface FileBrowserApi {
readonly openFileBrowser: (
kind: 'default' | 'directory' | 'file' | 'filePath',
defaultPath?: string,
fileTypes?: FileFilter[],
) => Promise<string[] | undefined>
}

Expand Down
2 changes: 1 addition & 1 deletion app/gui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ registerAutoBlurHandler()
registerGlobalBlurHandler()

const interactionBindingsHandler = interactionBindings.handler({
cancel: () => interaction.handleCancel(),
cancel: () => interaction.cancelAll(),
})

useEvent(window, 'keydown', interactionBindingsHandler)
Expand Down
12 changes: 12 additions & 0 deletions app/gui/src/project-view/components/ConditionalTeleport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
import { Opt } from '@/util/data/opt'

const props = defineProps<{ target: Opt<HTMLElement> }>()
</script>

<template>
<Teleport v-if="props.target" :to="props.target">
<slot />
</Teleport>
<slot v-else />
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,16 @@ function setPath(type: 'file' | 'secret', path: string) {

const write = computed(() => typeInfo.value.write)

const localBrowserItems = useLocalBrowser({ dialogKind, write, currentPath, setPath })
const cloudBrowserItems = useCloudBrowser({ dialogKind, write, currentPath, setPath })
const fileTypes = computed(() => {
if (props.input.dynamicConfig?.kind === 'File_Browse') {
return props.input.dynamicConfig?.file_types
} else {
return undefined
}
})

const localBrowserItems = useLocalBrowser({ dialogKind, write, currentPath, setPath, fileTypes })
const cloudBrowserItems = useCloudBrowser({ dialogKind, write, currentPath, setPath, fileTypes })
const textSecretsItems = useTextSecrets({ dialogKind, reprType })

const items = computed((): (CustomDropdownItem | ExpressionTag)[] => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
type CustomDropdownItem,
} from '@/components/GraphEditor/widgets/WidgetSelection/tags'
import FileBrowserWidget from '@/components/widgets/FileBrowserWidget.vue'
import { FileType } from '@/providers/widgetRegistry/configuration'
import { type Icon } from '@/util/iconMetadata/iconName'
import { type ToValue } from '@/util/reactivity'
import { computed, type ComputedRef, h, toValue } from 'vue'
import { Component, computed, type ComputedRef, h, toValue } from 'vue'

const TYPES = new Map<BrowserItem, { label: string; icon?: Icon }>([
['directory', { label: 'Choose directory in cloud…' }],
Expand All @@ -20,23 +21,27 @@ export function useCloudBrowser({
write,
currentPath,
setPath,
fileTypes,
}: {
dialogKind: ToValue<BrowserItem>
write: ToValue<boolean>
currentPath: ToValue<string | undefined>
setPath: (type: 'file' | 'secret', path: string) => void
fileTypes: ToValue<FileType[] | undefined>
}): ComputedRef<CustomDropdownItem[]> {
function openCloudBrowser({ setActivity, close }: Actions) {
setActivity(
computed(() => {
const type = toValue(dialogKind)
return h(FileBrowserWidget, {
return h(FileBrowserWidget as Component, {
type,
writeMode: toValue(write),
choosenPath: toValue(currentPath) ?? '',
onPathAccepted: (path: string) => {
setPath(type === 'secret' ? 'secret' : 'file', path)
close()
},
fileTypes: toValue(fileTypes),
onClose: close,
})
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type BrowserItem } from '@/components/GraphEditor/widgets/WidgetFileBrowser/browsableTypes'
import { type CustomDropdownItem } from '@/components/GraphEditor/widgets/WidgetSelection/tags'
import { FileType, isExtensions, isFileTypes } from '@/providers/widgetRegistry/configuration'
import { assert } from '@/util/assert'
import { FileFilter } from '@/util/fileFilter'
import { type ToValue } from '@/util/reactivity'
import { computed, toValue, type ComputedRef } from 'vue'

Expand All @@ -9,17 +11,33 @@ const LABELS = new Map<BrowserItem, string>([
['file', 'Choose file…'],
])

function fileTypesToFileFilters(fileTypes: FileType[]): FileFilter[] {
return fileTypes.flatMap((fileType) => {
const name = fileType.label
if (fileType.extensions.length > 0) {
if (isFileTypes(fileType.extensions)) {
return fileTypesToFileFilters(fileType.extensions)
} else if (isExtensions(fileType.extensions)) {
return [{ name, extensions: fileType.extensions }]
}
}
return []
})
}

/** @returns Dropdown items for opening a local file browser. */
export function useLocalBrowser({
dialogKind,
write,
currentPath,
setPath,
fileTypes,
}: {
dialogKind: ToValue<BrowserItem>
write: ToValue<boolean>
currentPath: ToValue<string | undefined>
setPath: (type: 'file', path: string) => void
fileTypes: ToValue<FileType[] | undefined>
}): ComputedRef<CustomDropdownItem[]> {
async function openFileBrowser() {
if (!window.fileBrowserApi) {
Expand All @@ -30,7 +48,13 @@ export function useLocalBrowser({
const rawKind = toValue(dialogKind)
assert(rawKind !== 'secret')
const kind = rawKind === 'file' && toValue(write) ? 'filePath' : rawKind
const selected = await window.fileBrowserApi.openFileBrowser(kind, toValue(currentPath))
const fileTypes_ = toValue(fileTypes)
const filters = fileTypes_ != null ? fileTypesToFileFilters(fileTypes_) : undefined
const selected = await window.fileBrowserApi.openFileBrowser(
kind,
toValue(currentPath),
filters,
)
if (selected != null && selected[0] != null) setPath('file', selected[0])
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,19 @@ const filteredTags = computed(() => {
return [...customTags, ...expressionTags]
}
})
const entries = computed<Entry[]>(() =>
filteredTags.value.map((tag) => ({

const entries = computed<Entry[]>(() => filteredTags.value.map(tagToEntry))

function tagToEntry(tag: ExpressionTag | NestedChoiceTag | ActionTag): Entry {
return {
value: tag.label,
selected: tag instanceof ExpressionTag && selectedExpressions.value.has(tag.expression),
icon: tag instanceof ExpressionTag || tag instanceof ActionTag ? tag.icon : undefined,
tag,
})),
)
isNested: tag instanceof NestedChoiceTag,
nestedValues: tag instanceof NestedChoiceTag ? tag.choices.map(tagToEntry) : [],
}
}

const removeSurroundingParens = (expr?: string) => expr?.trim().replaceAll(/(^[(])|([)]$)/g, '')

Expand Down Expand Up @@ -403,7 +408,10 @@ declare module '@/providers/widgetRegistry' {
:floatReference="floatReference"
:show="dropDownInteraction.isActive() && activity == null"
:entries="entries"
:selectedExpressions="selectedExpressions"
:isSelected="
(entry) =>
entry.tag instanceof ExpressionTag && selectedExpressions.has(entry.tag.expression)
"
:topLevel="true"
@clickedEntry="onClick"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
<script setup lang="ts">
<script setup lang="ts" generic="T extends SubmenuEntry<T>">
import ConditionalTeleport from '@/components/ConditionalTeleport.vue'
import SizeTransition from '@/components/SizeTransition.vue'
import DropdownWidget, { DropdownEntry } from '@/components/widgets/DropdownWidget.vue'
import { unrefElement } from '@/composables/events'
import { targetIsOutside } from '@/util/autoBlur'
import { computed, ComputedRef, ref, useTemplateRef, watch } from 'vue'
import { submenuDropdownStyles } from './styles'
import { Entry, ExpressionTag, isEntry, NestedChoiceTag } from './tags'
import { isSubmenuEntry, type SubmenuEntry } from './submenuEntry'

const props = defineProps<{
rootElement: HTMLElement | undefined
floatReference: HTMLElement | undefined
rootElement: HTMLElement | undefined | null
floatReference: HTMLElement | undefined | null
show: boolean
entries: Entry[]
selectedExpressions: Set<string>
entries: T[]
isSelected: (value: T) => boolean
topLevel?: boolean
color?: string | undefined
backgroundColor?: string | undefined
}>()

const emit = defineEmits<{
clickedEntry: [Entry, boolean]
clickedEntry: [T, boolean]
}>()

export interface Submenu {
entries: ComputedRef<Entry[]>
interface Submenu {
entries: ComputedRef<T[]>
relativeTo: HTMLElement
}

/** Referring to the type of the component in the current file is hard, so we define a helper type. */
interface SubmenuComponent {
isTargetOutside: (event: Event) => boolean
}
function isSubmenuComponent(component: unknown): component is SubmenuComponent {
return (
component != null &&
Expand All @@ -53,37 +52,26 @@ const { floatingStyles } = submenuDropdownStyles(
rootElement,
)

const nestedEntriesPresent = computed(() =>
props.entries.some((entry) => isEntry(entry) && entry.tag instanceof NestedChoiceTag),
)
const nestedEntriesPresent = computed(() => props.entries.some((entry) => entry.isNested))

function resetSubmenu() {
submenu.value = null
}
watch([() => props.show, () => props.entries], resetSubmenu)

function nestedChoiceTagToSubmenu(tag: NestedChoiceTag, target: HTMLElement): Submenu {
const isSelected = (tag: ExpressionTag | NestedChoiceTag) =>
tag instanceof ExpressionTag && props.selectedExpressions.has(tag.expression)
const choiceToEntry = (choice: ExpressionTag | NestedChoiceTag): Entry => ({
value: choice.label,
selected: isSelected(choice),
tag: choice,
})

function nestedEntryToSubmenu(entry: SubmenuEntry<T>, target: HTMLElement): Submenu {
return {
entries: computed(() => tag.choices.map(choiceToEntry) satisfies Entry[]),
entries: computed(() => entry.nestedValues satisfies SubmenuEntry<T>[]),
relativeTo: target,
}
}

function onClick(entry: DropdownEntry, keepOpen: boolean, htmlElement: HTMLElement) {
if (!isEntry(entry)) return
const tag = entry.tag
if (tag instanceof NestedChoiceTag) {
submenu.value = nestedChoiceTagToSubmenu(tag, htmlElement)
if (!isSubmenuEntry(entry)) return
if (entry.isNested) {
submenu.value = nestedEntryToSubmenu(entry as SubmenuEntry<T>, htmlElement)
} else {
emit('clickedEntry', entry, keepOpen)
emit('clickedEntry', entry as T, keepOpen)
}
}

Expand All @@ -102,32 +90,50 @@ function isTargetOutside(event: Event) {
defineExpose({
isTargetOutside,
})

defineOptions({
inheritAttrs: false,
})
</script>

<script lang="ts">
/** Referring to the type of the component in the current file is hard, so we define a helper type. */
export interface SubmenuComponent {
isTargetOutside: (event: Event) => boolean
}
</script>

<template>
<Teleport v-if="props.rootElement" :to="props.rootElement">
<div ref="dropdownElement" :style="floatingStyles" class="SelectionSubmenu widgetOutOfLayout">
<ConditionalTeleport :target="props.rootElement">
<div
ref="dropdownElement"
:style="floatingStyles"
class="SelectionSubmenu widgetOutOfLayout"
v-bind="$attrs"
>
<SizeTransition height :duration="100">
<DropdownWidget
v-if="props.show"
:class="{ ExtendUpwards: props.topLevel }"
color="var(--color-node-text)"
backgroundColor="var(--color-node-background)"
:color="props.color ?? 'var(--color-node-text)'"
:backgroundColor="props.backgroundColor ?? 'var(--color-node-background)'"
:entries="entries"
@clickEntry="onClick"
@scroll="onScroll"
/>
</SizeTransition>
</div>
</Teleport>
</ConditionalTeleport>
<SelectionSubmenu
v-if="nestedEntriesPresent"
ref="submenuRef"
:rootElement="props.rootElement"
:floatReference="submenu?.relativeTo"
:show="props.show && submenu != null"
:entries="submenuEntries"
:selectedExpressions="props.selectedExpressions"
:color="props.color ?? 'var(--color-node-text)'"
:backgroundColor="props.backgroundColor ?? 'var(--color-node-background)'"
:isSelected="props.isSelected"
@clickedEntry="(entry, keepOpen) => emit('clickedEntry', entry, keepOpen)"
/>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function submenuDropdownStyles(
floatReference: Ref<Opt<HTMLElement>>,
dropdownElement: Ref<Opt<HTMLElement>>,
isTopLevel: boolean,
rootElement: Ref<HTMLElement | undefined>,
rootElement: Ref<Opt<HTMLElement>>,
) {
return useFloating(floatReference, dropdownElement, {
placement: isTopLevel ? 'bottom-start' : 'right',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DropdownEntry } from '@/components/widgets/DropdownWidget.vue'

export interface SubmenuEntry<T> extends DropdownEntry {
isNested: boolean
get nestedValues(): T[]
}

/** Check if a {@link DropdownEntry} is a {@link SubmenuEntry}. */
export function isSubmenuEntry(entry: DropdownEntry): entry is SubmenuEntry<unknown> {
return 'isNested' in entry && 'nestedValues' in entry
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ProjectPath } from '@/util/projectPath'
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
import { type ToValue } from '@/util/reactivity'
import { VNode } from 'vue'
import { SubmenuEntry } from './submenuEntry'

/**
* The most basic dropdown item. When you click on it, the expression is inserted.
Expand Down Expand Up @@ -209,7 +210,7 @@ export interface Actions {
}

/** A helper type for all possible dropdown entries. */
export interface Entry extends DropdownEntry {
export interface Entry extends SubmenuEntry<Entry> {
tag: ExpressionTag | NestedChoiceTag | ActionTag
}

Expand Down
Loading
Loading
0