mirror of
https://github.com/nextcloud/server.git
synced 2026-06-08 16:26:59 -04:00
feat(files): propagate restore and delete events
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
parent
e85eb4c593
commit
f28944e23f
9 changed files with 168 additions and 89 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: () => ({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'] : [],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue