diff options
author | John Molakvoæ <skjnldsv@protonmail.com> | 2023-03-25 11:51:11 +0100 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2023-04-06 14:49:31 +0200 |
commit | f28944e23f96dd756cba3739e99c2fba57e81f1f (patch) | |
tree | e6fde2f68c458184d5f81d2c095f2903c0917e60 /apps | |
parent | e85eb4c59395c4c59d0bb19fb8ad64063a8d7f3b (diff) | |
download | nextcloud-server-f28944e23f96dd756cba3739e99c2fba57e81f1f.tar.gz nextcloud-server-f28944e23f96dd756cba3739e99c2fba57e81f1f.zip |
feat(files): propagate restore and delete events
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/actions/deleteAction.ts | 15 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 10 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 14 | ||||
-rw-r--r-- | apps/files/src/store/files.ts | 115 | ||||
-rw-r--r-- | apps/files/src/store/paths.ts | 56 | ||||
-rw-r--r-- | apps/files/src/store/sorting.ts | 13 | ||||
-rw-r--r-- | apps/files/src/types.ts | 12 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 4 | ||||
-rw-r--r-- | apps/files_trashbin/src/actions/restoreAction.ts | 14 |
9 files changed, 166 insertions, 87 deletions
diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index cd12c15ba10..acf855c8d15 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -19,28 +19,33 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ -import { registerFileAction, Permission, FileAction } from '@nextcloud/files' +import { registerFileAction, Permission, FileAction, Node } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import TrashCan from '@mdi/svg/svg/trash-can.svg?raw' -import logger from '../logger' +import { emit } from '@nextcloud/event-bus' registerFileAction(new FileAction({ id: 'delete', - displayName(nodes, view) { + displayName(nodes: Node[], view) { return view.id === 'trashbin' ? t('files_trashbin', 'Delete permanently') : t('files', 'Delete') }, iconSvgInline: () => TrashCan, - enabled(nodes) { + enabled(nodes: Node[]) { return nodes.length > 0 && nodes .map(node => node.permissions) .every(permission => (permission & Permission.DELETE) !== 0) }, - async exec(node) { + async exec(node: Node) { // No try...catch here, let the files app handle the error await axios.delete(node.source) + + // Let's delete even if it's moved to the trashbin + // since it has been removed from the current view + // and changing the view will trigger a reload anyway. + emit('files:file:deleted', node) return true }, order: 100, diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index 343d24f05e1..4d7582216f4 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -431,6 +431,16 @@ export default Vue.extend({ <style scoped lang='scss'> @import '../mixins/fileslist-row.scss'; +/* Hover effect on tbody lines only */ +tr { + &:hover, + &:focus, + &:active { + background-color: var(--color-background-dark); + } +} + +/* Preview not loaded animation effect */ .files-list__row-icon-preview:not([style*='background']) { background: linear-gradient(110deg, var(--color-loading-dark) 0%, var(--color-loading-dark) 25%, var(--color-loading-light) 50%, var(--color-loading-dark) 75%, var(--color-loading-dark) 100%); background-size: 400%; diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 77da9bb2d6f..e6cd60c2cad 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -139,6 +139,7 @@ export default Vue.extend({ height: 100%; &::v-deep { + // Table head, body and footer tbody, .vue-recycle-scroller__slot { display: flex; flex-direction: column; @@ -148,7 +149,7 @@ export default Vue.extend({ } // Table header - .vue-recycle-scroller__slot { + .vue-recycle-scroller__slot[role='thead'] { // Pinned on top when scrolling position: sticky; z-index: 10; @@ -157,18 +158,17 @@ export default Vue.extend({ background-color: var(--color-main-background); } + /** + * Common row styling. tr are handled by + * vue-virtual-scroller, so we need to + * have those rules in here. + */ tr { position: absolute; display: flex; align-items: center; width: 100%; border-bottom: 1px solid var(--color-border); - - &:hover, - &:focus, - &:active { - background-color: var(--color-background-dark); - } } } } diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts index 96752653735..f90f3bff7bb 100644 --- a/apps/files/src/store/files.ts +++ b/apps/files/src/store/files.ts @@ -24,51 +24,92 @@ import type { Folder, Node } from '@nextcloud/files' import type { FilesStore, RootsStore, RootOptions, Service, FilesState } from '../types' import { defineStore } from 'pinia' +import { subscribe } from '@nextcloud/event-bus' import Vue from 'vue' import logger from '../logger' -export const useFilesStore = defineStore('files', { - state: (): FilesState => ({ - files: {} as FilesStore, - roots: {} as RootsStore, - }), +export const useFilesStore = () => { + const store = defineStore('files', { + state: (): FilesState => ({ + files: {} as FilesStore, + roots: {} as RootsStore, + }), - getters: { - /** - * Get a file or folder by id - */ - getNode: (state) => (id: number): Node|undefined => state.files[id], + getters: { + /** + * Get a file or folder by id + */ + getNode: (state) => (id: number): Node|undefined => state.files[id], - /** - * Get a list of files or folders by their IDs - * Does not return undefined values - */ - getNodes: (state) => (ids: number[]): Node[] => ids - .map(id => state.files[id]) - .filter(Boolean), - /** - * Get a file or folder by id - */ - getRoot: (state) => (service: Service): Folder|undefined => state.roots[service], - }, + /** + * Get a list of files or folders by their IDs + * Does not return undefined values + */ + getNodes: (state) => (ids: number[]): Node[] => ids + .map(id => state.files[id]) + .filter(Boolean), + /** + * Get a file or folder by id + */ + getRoot: (state) => (service: Service): Folder|undefined => state.roots[service], + }, - actions: { - updateNodes(nodes: Node[]) { - // Update the store all at once - const files = nodes.reduce((acc, node) => { - if (!node.attributes.fileid) { - logger.warn('Trying to update/set a node without fileid', node) + actions: { + updateNodes(nodes: Node[]) { + // Update the store all at once + const files = nodes.reduce((acc, node) => { + if (!node.attributes.fileid) { + logger.warn('Trying to update/set a node without fileid', node) + return acc + } + acc[node.attributes.fileid] = node return acc - } - acc[node.attributes.fileid] = node - return acc - }, {} as FilesStore) + }, {} as FilesStore) - Vue.set(this, 'files', {...this.files, ...files}) - }, + Vue.set(this, 'files', {...this.files, ...files}) + }, + + deleteNodes(nodes: Node[]) { + nodes.forEach(node => { + if (node.fileid) { + Vue.delete(this.files, node.fileid) + } + }) + }, + + setRoot({ service, root }: RootOptions) { + Vue.set(this.roots, service, root) + }, + + onCreatedNode() { + // TODO: do something + }, - setRoot({ service, root }: RootOptions) { - Vue.set(this.roots, service, root) + onDeletedNode(node: Node) { + this.deleteNodes([node]) + }, + + onMovedNode() { + // TODO: do something + }, } + }) + + const fileStore = store() + // Make sure we only register the listeners once + if (!fileStore.initialized) { + subscribe('files:file:created', fileStore.onCreatedNode) + subscribe('files:file:deleted', fileStore.onDeletedNode) + subscribe('files:file:moved', fileStore.onMovedNode) + // subscribe('files:file:updated', fileStore.onUpdatedNode) + + subscribe('files:folder:created', fileStore.onCreatedNode) + subscribe('files:folder:deleted', fileStore.onDeletedNode) + subscribe('files:folder:moved', fileStore.onMovedNode) + // subscribe('files:folder:updated', fileStore.onUpdatedNode) + + fileStore.initialized = true } -}) + + return fileStore +} diff --git a/apps/files/src/store/paths.ts b/apps/files/src/store/paths.ts index 3573dd9c54e..43027390fe1 100644 --- a/apps/files/src/store/paths.ts +++ b/apps/files/src/store/paths.ts @@ -24,30 +24,46 @@ import type { PathOptions, ServicesState } from '../types' import { defineStore } from 'pinia' import Vue from 'vue' +import { subscribe } from '@nextcloud/event-bus' -export const usePathsStore = defineStore('paths', { - state: (): ServicesState => ({}), +export const usePathsStore = () => { + const store = defineStore('paths', { + state: (): ServicesState => ({}), - getters: { - getPath: (state) => { - return (service: string, path: string): number|undefined => { - if (!state[service]) { - return undefined + getters: { + getPath: (state) => { + return (service: string, path: string): number|undefined => { + if (!state[service]) { + return undefined + } + return state[service][path] } - return state[service][path] - } + }, }, - }, - actions: { - addPath(payload: PathOptions) { - // If it doesn't exists, init the service state - if (!this[payload.service]) { - Vue.set(this, payload.service, {}) - } + actions: { + addPath(payload: PathOptions) { + // If it doesn't exists, init the service state + if (!this[payload.service]) { + Vue.set(this, payload.service, {}) + } - // Now we can set the provided path - Vue.set(this[payload.service], payload.path, payload.fileid) - }, + // Now we can set the provided path + Vue.set(this[payload.service], payload.path, payload.fileid) + }, + } + }) + + const pathsStore = store() + // Make sure we only register the listeners once + if (!pathsStore.initialized) { + // TODO: watch folders to update paths? + // subscribe('files:folder:created', pathsStore.onCreatedNode) + // subscribe('files:folder:deleted', pathsStore.onDeletedNode) + // subscribe('files:folder:moved', pathsStore.onMovedNode) + + pathsStore.initialized = true } -}) + + return pathsStore +} diff --git a/apps/files/src/store/sorting.ts b/apps/files/src/store/sorting.ts index 8e7c87b12b3..dc83d100478 100644 --- a/apps/files/src/store/sorting.ts +++ b/apps/files/src/store/sorting.ts @@ -25,17 +25,7 @@ import { generateUrl } from '@nextcloud/router' import { defineStore } from 'pinia' import Vue from 'vue' import axios from '@nextcloud/axios' - -type direction = 'asc' | 'desc' - -interface SortingConfig { - mode: string - direction: direction -} - -interface SortingStore { - [key: string]: SortingConfig -} +import type { direction, SortingStore } from '../types' const saveUserConfig = (mode: string, direction: direction, view: string) => { return axios.post(generateUrl('/apps/files/api/v1/sorting'), { @@ -46,7 +36,6 @@ const saveUserConfig = (mode: string, direction: direction, view: string) => { } const filesSortingConfig = loadState('files', 'filesSortingConfig', {}) as SortingStore -console.debug('filesSortingConfig', filesSortingConfig) export const useSortingStore = defineStore('sorting', { state: () => ({ diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index a60b74294c0..207a45c080b 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -59,3 +59,15 @@ export interface PathOptions { path: string fileid: number } + +// Sorting store +export type direction = 'asc' | 'desc' + +export interface SortingConfig { + mode: string + direction: direction +} + +export interface SortingStore { + [key: string]: SortingConfig +} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 70f814870f6..b6310519e6b 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -173,13 +173,13 @@ export default Vue.extend({ // Custom column must provide their own sorting methods if (customColumn?.sort && typeof customColumn.sort === 'function') { - const results = [...(this.currentFolder?.children || []).map(this.getNode)] + const results = [...(this.currentFolder?.children || []).map(this.getNode).filter(file => file)] .sort(customColumn.sort) return this.isAscSorting ? results : results.reverse() } return orderBy( - [...(this.currentFolder?.children || []).map(this.getNode)], + [...(this.currentFolder?.children || []).map(this.getNode).filter(file => file)], [ // Sort folders first if sorting by name ...this.sortingMode === 'basename' ? [v => v.type !== 'folder'] : [], diff --git a/apps/files_trashbin/src/actions/restoreAction.ts b/apps/files_trashbin/src/actions/restoreAction.ts index d65ff3f0799..201fffe6a53 100644 --- a/apps/files_trashbin/src/actions/restoreAction.ts +++ b/apps/files_trashbin/src/actions/restoreAction.ts @@ -19,12 +19,13 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ -import { registerFileAction, Permission, FileAction } from '@nextcloud/files' +import { registerFileAction, Permission, FileAction, Node } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import History from '@mdi/svg/svg/history.svg?raw' import { generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' +import { emit } from '@nextcloud/event-bus' registerFileAction(new FileAction({ id: 'restore', @@ -32,7 +33,7 @@ registerFileAction(new FileAction({ return t('files_trashbin', 'Restore') }, iconSvgInline: () => History, - enabled(nodes, view) { + enabled(nodes: Node[], view) { // Only available in the trashbin view if (view.id !== 'trashbin') { return false @@ -43,15 +44,20 @@ registerFileAction(new FileAction({ .map(node => node.permissions) .every(permission => (permission & Permission.READ) !== 0) }, - async exec(node) { + async exec(node: Node) { // No try...catch here, let the files app handle the error + const destination = generateRemoteUrl(`dav/trashbin/${getCurrentUser()?.uid}/restore/${node.basename}`) await axios({ method: 'MOVE', url: node.source, headers: { - destination: generateRemoteUrl(`dav/trashbin/${getCurrentUser()?.uid}/restore/${node.basename}`), + destination, }, }) + + // Let's pretend the file is deleted since + // we don't know the restored location + emit('files:file:deleted', node) return true }, order: 1, |