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

feat: File browsers - add ability to quickly jump to any path segment #1659

Merged
Merged
14 changes: 11 additions & 3 deletions src/components/panels/GcodefilesPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,11 @@
<v-row>
<v-col class="col-12 py-2 d-flex align-center">
<span>
<b>{{ $t('Files.CurrentPath') }}:</b>
{{ currentPath || '/' }}
<b class="mr-1">{{ $t('Files.CurrentPath') }}:</b>
<path-navigation
:path="currentPath"
:base-directory-label="'/gcodes'"
:on-segment-click="clickPathNavGoToDirectory" />
</span>
<v-spacer></v-spacer>
<template v-if="disk_usage !== null">
Expand Down Expand Up @@ -652,6 +655,7 @@ import {
} from '@mdi/js'
import StartPrintDialog from '@/components/dialogs/StartPrintDialog.vue'
import ControlMixin from '@/components/mixins/control'
import PathNavigation from '@/components/ui/PathNavigation.vue'

interface contextMenu {
shown: boolean
Expand Down Expand Up @@ -694,7 +698,7 @@ interface tableColumnSetting {
}

@Component({
components: { StartPrintDialog, Panel, SettingsRow, draggable },
components: { StartPrintDialog, Panel, SettingsRow, PathNavigation, draggable },
})
export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
mdiChevronDown = mdiChevronDown
Expand Down Expand Up @@ -1255,6 +1259,10 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
this.currentPath = this.currentPath.slice(0, this.currentPath.lastIndexOf('/'))
}

clickPathNavGoToDirectory(segment: { location: string }) {
this.currentPath = segment.location
}

async addToQueue(item: FileStateGcodefile) {
let filename = [this.currentPath, item.filename].join('/')
if (filename.startsWith('/')) filename = filename.slice(1)
Expand Down
14 changes: 11 additions & 3 deletions src/components/panels/Machine/ConfigFilesPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@
<v-row>
<v-col class="col-12 py-2 d-flex align-center">
<span>
<b>{{ $t('Machine.ConfigFilesPanel.CurrentPath') }}:</b>
{{ absolutePath }}
<b class="mr-1">{{ $t('Machine.ConfigFilesPanel.CurrentPath') }}:</b>
<path-navigation
:path="currentPath"
:base-directory-label="`/${root}`"
:on-segment-click="clickPathNavGoToDirectory" />
</span>
<v-spacer></v-spacer>
<template v-if="disk_usage !== null && !showMissingConfigRootWarning">
Expand Down Expand Up @@ -537,6 +540,7 @@ import { formatFilesize, sortFiles } from '@/plugins/helpers'
import { FileStateFile, FileStateGcodefile } from '@/store/files/types'
import axios from 'axios'
import Panel from '@/components/ui/Panel.vue'
import PathNavigation from '@/components/ui/PathNavigation.vue'
import { hiddenRootDirectories } from '@/store/variables'
import {
mdiFilePlus,
Expand Down Expand Up @@ -607,7 +611,7 @@ interface draggingFile {
}

@Component({
components: { Panel },
components: { Panel, PathNavigation },
})
export default class ConfigFilesPanel extends Mixins(BaseMixin) {
mdiInformation = mdiInformation
Expand Down Expand Up @@ -1009,6 +1013,10 @@ export default class ConfigFilesPanel extends Mixins(BaseMixin) {
this.currentPath = this.currentPath.slice(0, this.currentPath.lastIndexOf('/'))
}

clickPathNavGoToDirectory(segment: { location: string }) {
this.currentPath = segment.location
}

showContextMenu(e: any, item: FileStateFile) {
if (!this.contextMenu.shown) {
e?.preventDefault()
Expand Down
26 changes: 22 additions & 4 deletions src/components/panels/Timelapse/TimelapseFilesPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@
<v-row>
<v-col class="col-12 py-2 d-flex align-center">
<span>
<b>{{ $t('Timelapse.CurrentPath') }}:</b>
{{ currentPath !== 'timelapse' ? '/' + currentPath.substring(10) : '/' }}
<b class="mr-1">{{ $t('Timelapse.CurrentPath') }}:</b>
<path-navigation
:path="currentPathForNavigation"
:base-directory-label="`/${rootDirectory}`"
:on-segment-click="clickPathNavGoToDirectory" />
</span>
<v-spacer></v-spacer>
<template v-if="disk_usage !== null">
Expand Down Expand Up @@ -107,7 +110,7 @@
<div class="text-center font-italic">{{ $t('Timelapse.Empty') }}</div>
</template>

<template v-if="currentPath !== 'timelapse'" slot="body.prepend">
<template v-if="currentPath !== rootDirectory" slot="body.prepend">
<tr class="file-list-cursor" @click="clickRowGoBack">
<td class="pr-0 text-center" style="width: 32px">
<v-icon>{{ mdiFolderUpload }}</v-icon>
Expand Down Expand Up @@ -423,6 +426,7 @@ import BaseMixin from '@/components/mixins/base'
import { formatFilesize, sortFiles } from '@/plugins/helpers'
import { FileStateFile, FileStateGcodefile } from '@/store/files/types'
import Panel from '@/components/ui/Panel.vue'
import PathNavigation from '@/components/ui/PathNavigation.vue'
import {
mdiFolderPlus,
mdiCloseThick,
Expand All @@ -446,7 +450,7 @@ interface dialogRenameObject {
}

@Component({
components: { Panel },
components: { Panel, PathNavigation },
})
export default class TimelapseFilesPanel extends Mixins(BaseMixin) {
formatFilesize = formatFilesize
Expand Down Expand Up @@ -537,6 +541,8 @@ export default class TimelapseFilesPanel extends Mixins(BaseMixin) {
(value: string) => !this.existsFilename(value) || this.$t('Files.InvalidNameAlreadyExists'),
]

private rootDirectory = 'timelapse'

existsFilename(name: string) {
return this.files.findIndex((file) => file.filename === name) >= 0
}
Expand Down Expand Up @@ -614,6 +620,14 @@ export default class TimelapseFilesPanel extends Mixins(BaseMixin) {
return this.$store.state.gui.view.timelapse.currentPath
}

get currentPathForNavigation() {
if (this.currentPath === this.rootDirectory) {
return ''
}

return this.currentPath.substring(this.rootDirectory.length)
}

set currentPath(newVal) {
this.$store.dispatch('gui/saveSettingWithoutUpload', { name: 'view.timelapse.currentPath', value: newVal })
}
Expand Down Expand Up @@ -690,6 +704,10 @@ export default class TimelapseFilesPanel extends Mixins(BaseMixin) {
this.currentPath = this.currentPath.slice(0, this.currentPath.lastIndexOf('/'))
}

clickPathNavGoToDirectory(segment: { location: string }) {
this.currentPath = `${this.rootDirectory}${segment.location}`
}

showContextMenu(e: any, item: FileStateFile) {
if (!this.contextMenu.shown) {
e?.preventDefault()
Expand Down
99 changes: 99 additions & 0 deletions src/components/ui/PathNavigation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<span>
<span v-for="({ directoryName, location }, index) in pathSegments" :key="location" class="navigation-container">
<template v-if="index !== 0">
<span class="navigation-divider text--disabled">{{ segmentSeparator }}</span>
</template>
<template v-if="index !== pathSegments.length - 1">
<span
class="cursor-pointer navigation-segment"
tabindex="0"
role="button"
@click="onSegmentClick({ location })"
@keyup.enter="onSegmentClick({ location })">
<template v-if="directoryName">{{ directoryName }}</template>
<template v-else>{{ baseDirectoryLabel }}</template>
</span>
</template>
<template v-else>
<span>
<template v-if="directoryName">{{ directoryName }}</template>
<template v-else>{{ baseDirectoryLabel }}</template>
</span>
</template>
</span>
</span>
</template>

<script lang="ts">
import Component from 'vue-class-component'
import { Mixins, Prop } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'

interface pathSegment {
directoryName: string
location: string
}

@Component
export default class PathNavigation extends Mixins(BaseMixin) {
/**
* Current path to be displayed in the breadcrumbs.
*/
@Prop({ default: false }) declare readonly path: string
/**
* Display label for the first directory in the path passed in absolute format
* (a path starting with `/` character). Useful for local paths, where
* the navigator deals with routes in some context (eg. all paths in the `/gcodes` directory).
*/
@Prop({ default: false }) declare readonly baseDirectoryLabel: string
/**
* Event handler triggered on breadcrumb segment interaction.
*
* @param segment.location Full location of the selected path segment,
* eg. for path `/foo/bar/baz`, when `bar` has been selected, `location` is equal to
* `/foo/bar`.
*/
@Prop({ default: false }) declare readonly onSegmentClick: (segment: { location: string }) => void

private readonly segmentSeparator = '/'

get pathSegments(): pathSegment[] {
const [firstSegment, ...restOfSegments] = (this.path || '').split(this.segmentSeparator)

const firstPathSegment = {
directoryName: firstSegment,
location: firstSegment,
}

return restOfSegments.reduce(
(allSegments: pathSegment[], currentSegment: string) => {
const previousSegmentLocation = allSegments[allSegments.length - 1].location
const location = `${previousSegmentLocation}${this.segmentSeparator}${currentSegment}`

const newPathSegment = {
directoryName: currentSegment,
location,
}

allSegments.push(newPathSegment)

return allSegments
},
[firstPathSegment]
)
}
}
</script>

<style scoped>
.navigation-divider {
padding: 0 2px;
}
.navigation-segment:hover {
text-decoration: underline;
}
.navigation-container:last-child {
font-weight: bold;
}
</style>
Loading