summaryrefslogtreecommitdiffstats
path: root/apps/files
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@users.noreply.github.com>2023-04-22 11:49:57 +0200
committerGitHub <noreply@github.com>2023-04-22 11:49:57 +0200
commit1b119e10d0249fbafb768cceef3c353b34b65d3a (patch)
tree7f7c62fb1bbfe57df8ef166694a6851abed0ea25 /apps/files
parent9e1703e76c974228e3534fae28c754e37ce55e2b (diff)
parent751bc139a1a418af4604c626311aef0b46c47a16 (diff)
downloadnextcloud-server-1b119e10d0249fbafb768cceef3c353b34b65d3a.tar.gz
nextcloud-server-1b119e10d0249fbafb768cceef3c353b34b65d3a.zip
Merge pull request #37866 from nextcloud/fix/files-vue
Diffstat (limited to 'apps/files')
-rw-r--r--apps/files/src/actions/deleteAction.ts7
-rw-r--r--apps/files/src/actions/sidebarAction.ts54
-rw-r--r--apps/files/src/components/FileEntry.vue41
-rw-r--r--apps/files/src/components/FilesListHeader.vue2
-rw-r--r--apps/files/src/components/FilesListHeaderActions.vue6
-rw-r--r--apps/files/src/components/FilesListVirtual.vue17
-rw-r--r--apps/files/src/main.ts3
-rw-r--r--apps/files/src/services/FileAction.ts5
-rw-r--r--apps/files/src/services/Navigation.ts7
-rw-r--r--apps/files/src/store/files.ts4
-rw-r--r--apps/files/src/store/paths.ts16
-rw-r--r--apps/files/src/types.ts10
-rw-r--r--apps/files/src/views/FilesList.vue8
-rw-r--r--apps/files/src/views/Navigation.vue2
14 files changed, 141 insertions, 41 deletions
diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts
index 087884b3362..a633e477b1f 100644
--- a/apps/files/src/actions/deleteAction.ts
+++ b/apps/files/src/actions/deleteAction.ts
@@ -27,10 +27,11 @@ import TrashCan from '@mdi/svg/svg/trash-can.svg?raw'
import { registerFileAction, FileAction } from '../services/FileAction.ts'
import logger from '../logger.js'
+import type { Navigation } from '../services/Navigation.ts'
registerFileAction(new FileAction({
id: 'delete',
- displayName(nodes: Node[], view) {
+ displayName(nodes: Node[], view: Navigation) {
return view.id === 'trashbin'
? t('files_trashbin', 'Delete permanently')
: t('files', 'Delete')
@@ -57,8 +58,8 @@ registerFileAction(new FileAction({
return false
}
},
- async execBatch(nodes: Node[], view) {
- return Promise.all(nodes.map(node => this.exec(node, view)))
+ async execBatch(nodes: Node[], view: Navigation, dir: string) {
+ return Promise.all(nodes.map(node => this.exec(node, view, dir)))
},
order: 100,
diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts
new file mode 100644
index 00000000000..f56d3a9475f
--- /dev/null
+++ b/apps/files/src/actions/sidebarAction.ts
@@ -0,0 +1,54 @@
+/**
+ * @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/>.
+ *
+ */
+import { translate as t } from '@nextcloud/l10n'
+import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw'
+import type { Node } from '@nextcloud/files'
+
+import { registerFileAction, FileAction } from '../services/FileAction.ts'
+import logger from '../logger.js'
+
+export const ACTION_DETAILS = 'details'
+
+registerFileAction(new FileAction({
+ id: ACTION_DETAILS,
+ displayName: () => t('files', 'Details'),
+ iconSvgInline: () => InformationSvg,
+
+ // Sidebar currently supports user folder only, /files/USER
+ enabled: (files: Node[]) => !!window?.OCA?.Files?.Sidebar
+ && files.some(node => node.root?.startsWith('/files/')),
+
+ async exec(node: Node) {
+ try {
+ // TODO: migrate Sidebar to use a Node instead
+ window?.OCA?.Files?.Sidebar?.open?.(node.path)
+
+ return null
+ } catch (error) {
+ logger.error('Error while opening sidebar', { error })
+ return false
+ }
+ },
+
+ default: true,
+ order: -50,
+}))
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index 00ff8a3d533..8dc067a407d 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -75,6 +75,7 @@
:container="boundariesElement"
:disabled="source._loading"
:force-title="true"
+ :force-menu="true"
:inline="enabledInlineActions.length"
:open.sync="openedMenu">
<NcActionButton v-for="action in enabledMenuActions"
@@ -94,7 +95,7 @@
<td v-if="isSizeAvailable"
:style="{ opacity: sizeOpacity }"
class="files-list__row-size"
- @click="execDefaultAction">
+ @click="openDetailsIfAvailable">
<span>{{ size }}</span>
</td>
@@ -103,7 +104,7 @@
:key="column.id"
:class="`files-list__row-${currentView?.id}-${column.id}`"
class="files-list__row-column-custom"
- @click="execDefaultAction">
+ @click="openDetailsIfAvailable">
<CustomElementRender v-if="active"
:current-view="currentView"
:render="column.render"
@@ -129,6 +130,7 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import StarIcon from 'vue-material-design-icons/Star.vue'
import Vue from 'vue'
+import { ACTION_DETAILS } from '../actions/sidebarAction.ts'
import { getFileActions } from '../services/FileAction.ts'
import { hashCode } from '../utils/hashUtils.ts'
import { isCachedPreview } from '../services/PreviewService.ts'
@@ -260,6 +262,15 @@ export default Vue.extend({
},
linkTo() {
+ if (this.source.type === 'folder') {
+ const to = { ...this.$route, query: { dir: join(this.dir, this.source.basename) } }
+ return {
+ is: 'router-link',
+ title: this.t('files', 'Open folder {name}', { name: this.displayName }),
+ to,
+ }
+ }
+
if (this.enabledDefaultActions.length > 0) {
const action = this.enabledDefaultActions[0]
const displayName = action.displayName([this.source], this.currentView)
@@ -269,14 +280,6 @@ export default Vue.extend({
}
}
- if (this.source.type === 'folder') {
- const to = { ...this.$route, query: { dir: join(this.dir, this.source.basename) } }
- return {
- is: 'router-link',
- title: this.t('files', 'Open folder {name}', { name: this.displayName }),
- to,
- }
- }
return {
href: this.source.source,
// TODO: Use first action title ?
@@ -501,7 +504,7 @@ export default Vue.extend({
this.loading = action.id
Vue.set(this.source, '_loading', true)
- const success = await action.exec(this.source, this.currentView)
+ const success = await action.exec(this.source, this.currentView, this.dir)
// If the action returns null, we stay silent
if (success === null) {
@@ -523,11 +526,25 @@ export default Vue.extend({
}
},
execDefaultAction(event) {
+ // Do not execute the default action on the folder, navigate instead
+ if (this.source.type === 'folder') {
+ return
+ }
+
if (this.enabledDefaultActions.length > 0) {
event.preventDefault()
event.stopPropagation()
// Execute the first default action if any
- this.enabledDefaultActions[0].exec(this.source, this.currentView)
+ this.enabledDefaultActions[0].exec(this.source, this.currentView, this.dir)
+ }
+ },
+
+ openDetailsIfAvailable(event) {
+ const detailsAction = this.enabledDefaultActions.find(action => action.id === ACTION_DETAILS)
+ if (detailsAction) {
+ event.preventDefault()
+ event.stopPropagation()
+ detailsAction.exec(this.source, this.currentView)
}
},
diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue
index 9e3fe0d46de..2d848d0eefe 100644
--- a/apps/files/src/components/FilesListHeader.vue
+++ b/apps/files/src/components/FilesListHeader.vue
@@ -173,7 +173,7 @@ export default Vue.extend({
onToggleAll(selected) {
if (selected) {
- const selection = this.nodes.map(node => node.attributes.fileid.toString())
+ const selection = this.nodes.map(node => node.fileid.toString())
logger.debug('Added all nodes to selection', { selection })
this.selectionStore.setLastIndex(null)
this.selectionStore.set(selection)
diff --git a/apps/files/src/components/FilesListHeaderActions.vue b/apps/files/src/components/FilesListHeaderActions.vue
index a53a1d041bf..f8c60a5cd1b 100644
--- a/apps/files/src/components/FilesListHeaderActions.vue
+++ b/apps/files/src/components/FilesListHeaderActions.vue
@@ -103,6 +103,10 @@ export default Vue.extend({
},
computed: {
+ dir() {
+ // Remove any trailing slash but leave root slash
+ return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
+ },
enabledActions() {
return actions
.filter(action => action.execBatch)
@@ -165,7 +169,7 @@ export default Vue.extend({
})
// Dispatch action execution
- const results = await action.execBatch(this.nodes, this.currentView)
+ const results = await action.execBatch(this.nodes, this.currentView, this.dir)
// Check if all actions returned null
if (!results.some(result => result !== null)) {
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index ad0ba2069ff..866fc6da00d 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -139,7 +139,7 @@ export default Vue.extend({
methods: {
getFileId(node) {
- return node.attributes.fileid
+ return node.fileid
},
t: translate,
@@ -233,22 +233,24 @@ export default Vue.extend({
}
.files-list__row-icon {
+ position: relative;
display: flex;
+ overflow: visible;
align-items: center;
+ // No shrinking or growing allowed
+ flex: 0 0 var(--icon-preview-size);
justify-content: center;
width: var(--icon-preview-size);
height: 100%;
// Show same padding as the checkbox right padding for visual balance
margin-right: var(--checkbox-padding);
color: var(--color-primary-element);
- // No shrinking or growing allowed
- flex: 0 0 var(--icon-preview-size);
& > span {
justify-content: flex-start;
}
- svg {
+ &> span:not(.files-list__row-icon-favorite) svg {
width: var(--icon-preview-size);
height: var(--icon-preview-size);
}
@@ -263,6 +265,13 @@ export default Vue.extend({
background-position: center;
background-size: contain;
}
+
+ &-favorite {
+ position: absolute;
+ top: 4px;
+ right: -8px;
+ color: #ffcc00;
+ }
}
.files-list__row-name {
diff --git a/apps/files/src/main.ts b/apps/files/src/main.ts
index 976714a8f1f..195357d0e0a 100644
--- a/apps/files/src/main.ts
+++ b/apps/files/src/main.ts
@@ -1,6 +1,7 @@
import './templates.js'
import './legacy/filelistSearch.js'
import './actions/deleteAction'
+import './actions/sidebarAction'
import Vue from 'vue'
import { createPinia, PiniaVuePlugin } from 'pinia'
@@ -11,9 +12,9 @@ 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 RouterService from './services/RouterService'
import SettingsModel from './models/Setting.js'
import SettingsService from './services/Settings.js'
-import RouterService from './services/RouterService'
declare global {
interface Window {
diff --git a/apps/files/src/services/FileAction.ts b/apps/files/src/services/FileAction.ts
index 94fc7e8ce5f..70d6405c804 100644
--- a/apps/files/src/services/FileAction.ts
+++ b/apps/files/src/services/FileAction.ts
@@ -22,6 +22,7 @@
import type { Node } from '@nextcloud/files'
import logger from '../logger'
+import type { Navigation } from './Navigation'
declare global {
interface Window {
@@ -48,14 +49,14 @@ interface FileActionData {
* @returns true if the action was executed, false otherwise
* @throws Error if the action failed
*/
- exec: (file: Node, view) => Promise<boolean|null>,
+ exec: (file: Node, view: Navigation, dir: string) => Promise<boolean|null>,
/**
* Function executed on multiple files action
* @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|null)[]>
+ execBatch?: (files: Node[], view: Navigation, dir: string) => Promise<(boolean|null)[]>
/** This action order in the list */
order?: number,
/** Make this action the default */
diff --git a/apps/files/src/services/Navigation.ts b/apps/files/src/services/Navigation.ts
index e86266013d7..b2ae3b0b973 100644
--- a/apps/files/src/services/Navigation.ts
+++ b/apps/files/src/services/Navigation.ts
@@ -126,6 +126,13 @@ export default class {
this._views.push(view)
}
+ remove(id: string) {
+ const index = this._views.findIndex(view => view.id === id)
+ if (index !== -1) {
+ this._views.splice(index, 1)
+ }
+ }
+
get views(): Navigation[] {
return this._views
}
diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts
index ea516a886d9..bd7d3202dd9 100644
--- a/apps/files/src/store/files.ts
+++ b/apps/files/src/store/files.ts
@@ -59,11 +59,11 @@ export const useFilesStore = function() {
updateNodes(nodes: Node[]) {
// Update the store all at once
const files = nodes.reduce((acc, node) => {
- if (!node.attributes.fileid) {
+ if (!node.fileid) {
logger.warn('Trying to update/set a node without fileid', node)
return acc
}
- acc[node.attributes.fileid] = node
+ acc[node.fileid] = node
return acc
}, {} as FilesStore)
diff --git a/apps/files/src/store/paths.ts b/apps/files/src/store/paths.ts
index c9335304bce..ecff97bf00c 100644
--- a/apps/files/src/store/paths.ts
+++ b/apps/files/src/store/paths.ts
@@ -24,20 +24,22 @@ import type { PathOptions, ServicesState } from '../types.ts'
import { defineStore } from 'pinia'
import { subscribe } from '@nextcloud/event-bus'
-import type { FileId } from '../types'
+import type { FileId, PathsStore } from '../types'
import Vue from 'vue'
export const usePathsStore = function() {
const store = defineStore('paths', {
- state: (): ServicesState => ({}),
+ state: () => ({
+ paths: {} as ServicesState
+ } as PathsStore),
getters: {
getPath: (state) => {
return (service: string, path: string): FileId|undefined => {
- if (!state[service]) {
+ if (!state.paths[service]) {
return undefined
}
- return state[service][path]
+ return state.paths[service][path]
}
},
},
@@ -45,12 +47,12 @@ export const usePathsStore = function() {
actions: {
addPath(payload: PathOptions) {
// If it doesn't exists, init the service state
- if (!this[payload.service]) {
- Vue.set(this, payload.service, {})
+ if (!this.paths[payload.service]) {
+ Vue.set(this.paths, payload.service, {})
}
// Now we can set the provided path
- Vue.set(this[payload.service], payload.path, payload.fileid)
+ Vue.set(this.paths[payload.service], payload.path, payload.fileid)
},
}
})
diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts
index cca6fb9111f..c04a9538827 100644
--- a/apps/files/src/types.ts
+++ b/apps/files/src/types.ts
@@ -49,13 +49,17 @@ export interface RootOptions {
// Paths store
export type ServicesState = {
- [service: Service]: PathsStore
+ [service: Service]: PathConfig
}
-export type PathsStore = {
+export type PathConfig = {
[path: string]: number
}
+export type PathsStore = {
+ paths: ServicesState
+}
+
export interface PathOptions {
service: Service
path: string
@@ -91,4 +95,4 @@ export interface ViewConfigs {
}
export interface ViewConfigStore {
viewConfig: ViewConfigs
-} \ No newline at end of file
+}
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index 50f35fef5aa..f2a20c18f28 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -268,16 +268,16 @@ export default Vue.extend({
this.filesStore.updateNodes(contents)
// Define current directory children
- folder._children = contents.map(node => node.attributes.fileid)
+ folder._children = contents.map(node => node.fileid)
// If we're in the root dir, define the root
if (dir === '/') {
this.filesStore.setRoot({ service: currentView.id, root: folder })
} else
// Otherwise, add the folder to the store
- if (folder.attributes.fileid) {
+ if (folder.fileid) {
this.filesStore.updateNodes([folder])
- this.pathsStore.addPath({ service: currentView.id, fileid: folder.attributes.fileid, path: dir })
+ this.pathsStore.addPath({ service: currentView.id, fileid: folder.fileid, path: dir })
} else {
// If we're here, the view API messed up
logger.error('Invalid root folder returned', { dir, folder, currentView })
@@ -286,7 +286,7 @@ export default Vue.extend({
// Update paths store
const folders = contents.filter(node => node.type === 'folder')
folders.forEach(node => {
- this.pathsStore.addPath({ service: currentView.id, fileid: node.attributes.fileid, path: join(dir, node.basename) })
+ this.pathsStore.addPath({ service: currentView.id, fileid: node.fileid, path: join(dir, node.basename) })
})
} catch (error) {
logger.error('Error while fetching content', { error })
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue
index e5556e88958..e164880003a 100644
--- a/apps/files/src/views/Navigation.vue
+++ b/apps/files/src/views/Navigation.vue
@@ -44,7 +44,7 @@
:title="child.name"
:to="generateToNavigation(child)">
<!-- Sanitized icon as svg if provided -->
- <NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" />
+ <NcIconSvgWrapper v-if="child.icon" slot="icon" :svg="child.icon" />
</NcAppNavigationItem>
</NcAppNavigationItem>
</template>