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(files): add default action support and expose router #37824

Merged
merged 2 commits into from
Apr 20, 2023
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
93 changes: 67 additions & 26 deletions apps/files/src/components/FileEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

<!-- Link to file -->
<td class="files-list__row-name">
<a ref="name" v-bind="linkTo">
<a ref="name" v-bind="linkTo" @click="execDefaultAction">
<!-- Icon or preview -->
<span class="files-list__row-icon">
<FolderIcon v-if="source.type === 'folder'" />
Expand All @@ -49,6 +49,13 @@
:style="{ backgroundImage: mimeIconUrl }" />

<FileIcon v-else />

<!-- Favorite icon -->
<span v-if="isFavorite"
class="files-list__row-icon-favorite"
:aria-label="t('files', 'Favorite')">
<StarIcon aria-hidden="true" :size="20" />
</span>
</span>

<!-- File name -->
Expand All @@ -64,6 +71,8 @@
<!-- Menu actions -->
<NcActions v-if="active"
ref="actionsMenu"
:boundaries-element="boundariesElement"
:container="boundariesElement"
:disabled="source._loading"
:force-title="true"
:inline="enabledInlineActions.length"
Expand All @@ -84,15 +93,17 @@
<!-- Size -->
<td v-if="isSizeAvailable"
:style="{ opacity: sizeOpacity }"
class="files-list__row-size">
class="files-list__row-size"
@click="execDefaultAction">
<span>{{ size }}</span>
</td>

<!-- View columns -->
<td v-for="column in columns"
:key="column.id"
:class="`files-list__row-${currentView?.id}-${column.id}`"
class="files-list__row-column-custom">
class="files-list__row-column-custom"
@click="execDefaultAction">
<CustomElementRender v-if="active"
:current-view="currentView"
:render="column.render"
Expand All @@ -115,9 +126,11 @@ import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import StarIcon from 'vue-material-design-icons/Star.vue'
import Vue from 'vue'

import { getFileActions } from '../services/FileAction.ts'
import { hashCode } from '../utils/hashUtils.ts'
import { isCachedPreview } from '../services/PreviewService.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useFilesStore } from '../store/files.ts'
Expand All @@ -144,6 +157,7 @@ export default Vue.extend({
NcActions,
NcCheckboxRadioSwitch,
NcLoadingIcon,
StarIcon,
},

props: {
Expand Down Expand Up @@ -192,6 +206,7 @@ export default Vue.extend({
return {
backgroundFailed: false,
backgroundImage: '',
boundariesElement: document.querySelector('.app-content > .files-list'),
loading: '',
}
},
Expand All @@ -204,7 +219,6 @@ export default Vue.extend({
currentView() {
return this.$navigation.active
},

columns() {
// Hide columns if the list is too small
if (this.filesListWidth < 512) {
Expand All @@ -217,22 +231,21 @@ export default Vue.extend({
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
},

fileid() {
return this.source?.fileid?.toString?.()
},
displayName() {
return this.source.attributes.displayName
|| this.source.basename
},

size() {
const size = parseInt(this.source.size, 10) || 0
if (typeof size !== 'number' || size < 0) {
return this.t('files', 'Pending')
}
return formatFileSize(size, true)
},

sizeOpacity() {
const size = parseInt(this.source.size, 10) || 0
if (!size || size < 0) {
Expand All @@ -247,6 +260,15 @@ export default Vue.extend({
},

linkTo() {
if (this.enabledDefaultActions.length > 0) {
const action = this.enabledDefaultActions[0]
const displayName = action.displayName([this.source], this.currentView)
return {
title: displayName,
role: 'button',
}
}

if (this.source.type === 'folder') {
const to = { ...this.$route, query: { dir: join(this.dir, this.source.basename) } }
return {
Expand All @@ -272,21 +294,19 @@ export default Vue.extend({
cropPreviews() {
return this.userConfig.crop_image_previews
},

previewUrl() {
try {
const url = new URL(window.location.origin + this.source.attributes.previewUrl)
// Request tiny previews
url.searchParams.set('x', '32')
url.searchParams.set('y', '32')
// Handle cropping
url.searchParams.set('a', this.cropPreviews === true ? '1' : '0')
url.searchParams.set('a', this.cropPreviews === true ? '0' : '1')
return url.href
} catch (e) {
return null
}
},

mimeIconUrl() {
const mimeType = this.source.mime || 'application/octet-stream'
const mimeIconUrl = window.OC?.MimeType?.getIconUrl?.(mimeType)
Expand All @@ -301,29 +321,38 @@ export default Vue.extend({
.filter(action => !action.enabled || action.enabled([this.source], this.currentView))
.sort((a, b) => (a.order || 0) - (b.order || 0))
},

enabledInlineActions() {
if (this.filesListWidth < 768) {
return []
}
return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView))
},

enabledMenuActions() {
if (this.filesListWidth < 768) {
// If we have a default action, do not render the first one
if (this.enabledDefaultActions.length > 0) {
return this.enabledActions.slice(1)
}
return this.enabledActions
}

return [
const actions = [
...this.enabledInlineActions,
...this.enabledActions.filter(action => !action.inline),
]
},

uniqueId() {
return this.hashCode(this.source.source)
},
// If we have a default action, do not render the first one
if (this.enabledDefaultActions.length > 0) {
return actions.slice(1)
}

return actions
},
enabledDefaultActions() {
return [
...this.enabledActions.filter(action => action.default),
]
},
openedMenu: {
get() {
return this.actionsMenuStore.opened === this.uniqueId
Expand All @@ -332,6 +361,14 @@ export default Vue.extend({
this.actionsMenuStore.opened = opened ? this.uniqueId : null
},
},

uniqueId() {
return hashCode(this.source.source)
},

isFavorite() {
return this.source.attributes.favorite === 1
},
},

watch: {
Expand Down Expand Up @@ -457,16 +494,6 @@ export default Vue.extend({
}
},

hashCode(str) {
let hash = 0
for (let i = 0, len = str.length; i < len; i++) {
const chr = str.charCodeAt(i)
hash = (hash << 5) - hash + chr
hash |= 0 // Convert to 32bit integer
}
return hash
},

async onActionClick(action) {
const displayName = action.displayName([this.source], this.currentView)
try {
Expand All @@ -475,6 +502,12 @@ export default Vue.extend({
Vue.set(this.source, '_loading', true)

const success = await action.exec(this.source, this.currentView)

// If the action returns null, we stay silent
if (success === null) {
return
}

if (success) {
showSuccess(this.t('files', '"{displayName}" action executed successfully', { displayName }))
return
Expand All @@ -489,6 +522,14 @@ export default Vue.extend({
Vue.set(this.source, '_loading', false)
}
},
execDefaultAction(event) {
if (this.enabledDefaultActions.length > 0) {
event.preventDefault()
event.stopPropagation()
// Execute the first default action if any
this.enabledDefaultActions[0].exec(this.source, this.currentView)
}
},

onSelectionChange(selection) {
const newSelectedIndex = this.index
Expand Down
11 changes: 9 additions & 2 deletions apps/files/src/components/FilesListHeaderActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,18 @@ export default Vue.extend({
// Dispatch action execution
const results = await action.execBatch(this.nodes, this.currentView)

// Check if all actions returned null
if (!results.some(result => result !== null)) {
// If the actions returned null, we stay silent
this.selectionStore.reset()
return
}

// Handle potential failures
if (results.some(result => result !== true)) {
if (results.some(result => result === false)) {
// Remove the failed ids from the selection
const failedIds = selectionIds
.filter((fileid, index) => results[index] !== true)
.filter((fileid, index) => results[index] === false)
this.selectionStore.set(failedIds)

showError(this.t('files', '"{displayName}" failed on some elements ', { displayName }))
Expand Down
31 changes: 20 additions & 11 deletions apps/files/src/main.js → apps/files/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
import './templates.js'
import './legacy/filelistSearch.js'
import './actions/deleteAction.ts'

import processLegacyFilesViews from './legacy/navigationMapper.js'
import './actions/deleteAction'

import Vue from 'vue'
import { createPinia, PiniaVuePlugin } from 'pinia'

import NavigationService from './services/Navigation.ts'
import registerPreviewServiceWorker from './services/ServiceWorker.js'

import NavigationView from './views/Navigation.vue'
import FilesListView from './views/FilesList.vue'

import SettingsService from './services/Settings.js'
import NavigationService from './services/Navigation'
import NavigationView from './views/Navigation.vue'
import processLegacyFilesViews from './legacy/navigationMapper.js'
import registerPreviewServiceWorker from './services/ServiceWorker.js'
import router from './router/router.js'
import SettingsModel from './models/Setting.js'
import SettingsService from './services/Settings.js'
import RouterService from './services/RouterService'

import router from './router/router.js'
declare global {
interface Window {
OC: any;
OCA: any;
OCP: any;
}
}

// Init private and public Files namespace
window.OCA.Files = window.OCA.Files ?? {}
window.OCP.Files = window.OCP.Files ?? {}

// Expose router
const Router = new RouterService(router)
Object.assign(window.OCP.Files, { Router })

// Init Pinia store
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
Expand Down Expand Up @@ -57,7 +66,7 @@ const FilesList = new ListView({
})
FilesList.$mount('#app-content-vue')

// Init legacy files views
// Init legacy and new files views
processLegacyFilesViews()

// Register preview service worker
Expand Down
9 changes: 5 additions & 4 deletions apps/files/src/services/FileAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*
*/

import { Node } from '@nextcloud/files'
import type { Node } from '@nextcloud/files'
import logger from '../logger'

declare global {
Expand Down Expand Up @@ -48,13 +48,14 @@ interface FileActionData {
* @returns true if the action was executed, false otherwise
* @throws Error if the action failed
*/
exec: (file: Node, view) => Promise<boolean>,
exec: (file: Node, view) => Promise<boolean|null>,
/**
* Function executed on multiple files action
* @returns true if the action was executed, false otherwise
* @returns true if the action was executed successfully,
* false otherwise and null if the action is silent/undefined.
* @throws Error if the action failed
*/
execBatch?: (files: Node[], view) => Promise<boolean[]>
execBatch?: (files: Node[], view) => Promise<(boolean|null)[]>
/** This action order in the list */
order?: number,
/** Make this action the default */
Expand Down
Loading