summaryrefslogtreecommitdiffstats
path: root/apps/files
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@protonmail.com>2023-04-18 09:43:29 +0200
committerJohn Molakvoæ <skjnldsv@protonmail.com>2023-04-20 09:06:57 +0200
commitbb4d7969b93c806e4f578ecc5a6d04bb6bebee73 (patch)
treee6247c554d7e136042e0913f370f37ff1467ac37 /apps/files
parentc85c04e4a8495eb04419a27a8e162c03acad6282 (diff)
downloadnextcloud-server-bb4d7969b93c806e4f578ecc5a6d04bb6bebee73.tar.gz
nextcloud-server-bb4d7969b93c806e4f578ecc5a6d04bb6bebee73.zip
feat(files): add default action support
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files')
-rw-r--r--apps/files/src/components/FileEntry.vue93
-rw-r--r--apps/files/src/components/FilesListHeaderActions.vue11
-rw-r--r--apps/files/src/main.js5
-rw-r--r--apps/files/src/services/FileAction.ts7
-rw-r--r--apps/files/src/utils/hashUtils.ts28
-rw-r--r--apps/files/src/views/FilesList.vue2
-rw-r--r--apps/files/src/views/Navigation.vue1
7 files changed, 113 insertions, 34 deletions
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index 7db22482220..00ff8a3d533 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -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'" />
@@ -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 -->
@@ -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"
@@ -84,7 +93,8 @@
<!-- Size -->
<td v-if="isSizeAvailable"
:style="{ opacity: sizeOpacity }"
- class="files-list__row-size">
+ class="files-list__row-size"
+ @click="execDefaultAction">
<span>{{ size }}</span>
</td>
@@ -92,7 +102,8 @@
<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"
@@ -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'
@@ -144,6 +157,7 @@ export default Vue.extend({
NcActions,
NcCheckboxRadioSwitch,
NcLoadingIcon,
+ StarIcon,
},
props: {
@@ -192,6 +206,7 @@ export default Vue.extend({
return {
backgroundFailed: false,
backgroundImage: '',
+ boundariesElement: document.querySelector('.app-content > .files-list'),
loading: '',
}
},
@@ -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) {
@@ -217,7 +231,6 @@ 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?.()
},
@@ -225,6 +238,7 @@ export default Vue.extend({
return this.source.attributes.displayName
|| this.source.basename
},
+
size() {
const size = parseInt(this.source.size, 10) || 0
if (typeof size !== 'number' || size < 0) {
@@ -232,7 +246,6 @@ export default Vue.extend({
}
return formatFileSize(size, true)
},
-
sizeOpacity() {
const size = parseInt(this.source.size, 10) || 0
if (!size || size < 0) {
@@ -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 {
@@ -272,7 +294,6 @@ export default Vue.extend({
cropPreviews() {
return this.userConfig.crop_image_previews
},
-
previewUrl() {
try {
const url = new URL(window.location.origin + this.source.attributes.previewUrl)
@@ -280,13 +301,12 @@ export default Vue.extend({
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)
@@ -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
@@ -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: {
@@ -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 {
@@ -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
@@ -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
diff --git a/apps/files/src/components/FilesListHeaderActions.vue b/apps/files/src/components/FilesListHeaderActions.vue
index c9f0c66be03..b86d1f1d80b 100644
--- a/apps/files/src/components/FilesListHeaderActions.vue
+++ b/apps/files/src/components/FilesListHeaderActions.vue
@@ -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.filter(result => result !== null).length === 0) {
+ // 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 }))
diff --git a/apps/files/src/main.js b/apps/files/src/main.js
index a8464f0ee0d..db1726e3376 100644
--- a/apps/files/src/main.js
+++ b/apps/files/src/main.js
@@ -22,6 +22,9 @@ import router from './router/router.js'
window.OCA.Files = window.OCA.Files ?? {}
window.OCP.Files = window.OCP.Files ?? {}
+// Expose router
+Object.assign(window.OCP.Files, { Router: router })
+
// Init Pinia store
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
@@ -57,7 +60,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
diff --git a/apps/files/src/services/FileAction.ts b/apps/files/src/services/FileAction.ts
index 8c1d325e645..453dbe535ee 100644
--- a/apps/files/src/services/FileAction.ts
+++ b/apps/files/src/services/FileAction.ts
@@ -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 */
diff --git a/apps/files/src/utils/hashUtils.ts b/apps/files/src/utils/hashUtils.ts
new file mode 100644
index 00000000000..55cf8b9f51a
--- /dev/null
+++ b/apps/files/src/utils/hashUtils.ts
@@ -0,0 +1,28 @@
+/**
+ * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+export const hashCode = function(str: string): number {
+ return str.split('').reduce(function(a, b) {
+ a = ((a << 5) - a) + b.charCodeAt(0)
+ return a & a
+ }, 0)
+}
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index c11b5820308..50f35fef5aa 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -166,7 +166,7 @@ export default Vue.extend({
return []
}
- const customColumn = this.currentView.columns
+ const customColumn = (this.currentView?.columns || [])
.find(column => column.id === this.sortingMode)
// Custom column must provide their own sorting methods
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue
index cc714964c9b..e5556e88958 100644
--- a/apps/files/src/views/Navigation.vue
+++ b/apps/files/src/views/Navigation.vue
@@ -175,7 +175,6 @@ export default {
this.Navigation.setActive(view)
logger.debug('Navigation changed', { id: view.id, view })
- // debugger
this.showView(view, oldView)
},
},