summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@protonmail.com>2023-03-25 11:51:11 +0100
committerJohn Molakvoæ <skjnldsv@protonmail.com>2023-04-06 14:49:31 +0200
commitf28944e23f96dd756cba3739e99c2fba57e81f1f (patch)
treee6fde2f68c458184d5f81d2c095f2903c0917e60 /apps
parente85eb4c59395c4c59d0bb19fb8ad64063a8d7f3b (diff)
downloadnextcloud-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.ts15
-rw-r--r--apps/files/src/components/FileEntry.vue10
-rw-r--r--apps/files/src/components/FilesListVirtual.vue14
-rw-r--r--apps/files/src/store/files.ts115
-rw-r--r--apps/files/src/store/paths.ts56
-rw-r--r--apps/files/src/store/sorting.ts13
-rw-r--r--apps/files/src/types.ts12
-rw-r--r--apps/files/src/views/FilesList.vue4
-rw-r--r--apps/files_trashbin/src/actions/restoreAction.ts14
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,