feat(files): propagate restore and delete events

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ 2023-03-25 11:51:11 +01:00
parent e85eb4c593
commit f28944e23f
No known key found for this signature in database
GPG key ID: 60C25B8C072916CF
9 changed files with 168 additions and 89 deletions

View file

@ -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,

View file

@ -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%;

View file

@ -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);
}
}
}
}

View file

@ -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],
},
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
}, {} as FilesStore)
Vue.set(this, 'files', {...this.files, ...files})
/**
* 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],
},
setRoot({ service, root }: RootOptions) {
Vue.set(this.roots, service, root)
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
}, {} as FilesStore)
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
},
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
}

View file

@ -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
}

View file

@ -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: () => ({

View file

@ -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
}

View file

@ -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'] : [],

View file

@ -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,