Skip to content

Commit c0bdd75

Browse files
committed
fix(files): properly update paths and folder children on node move
Signed-off-by: skjnldsv <[email protected]>
1 parent d334773 commit c0bdd75

File tree

3 files changed

+114
-61
lines changed

3 files changed

+114
-61
lines changed

apps/files/src/store/files.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Vue from 'vue'
1313

1414
import { fetchNode } from '../services/WebdavClient.ts'
1515
import { usePathsStore } from './paths.ts'
16+
import { dirname } from '@nextcloud/paths'
1617

1718
export const useFilesStore = function(...args) {
1819
const store = defineStore('files', {
@@ -24,14 +25,12 @@ export const useFilesStore = function(...args) {
2425
getters: {
2526
/**
2627
* Get a file or folder by its source
27-
* @param state
2828
*/
2929
getNode: (state) => (source: FileSource): Node|undefined => state.files[source],
3030

3131
/**
3232
* Get a list of files or folders by their IDs
3333
* Note: does not return undefined values
34-
* @param state
3534
*/
3635
getNodes: (state) => (sources: FileSource[]): Node[] => sources
3736
.map(source => state.files[source])
@@ -41,13 +40,11 @@ export const useFilesStore = function(...args) {
4140
* Get files or folders by their file ID
4241
* Multiple nodes can have the same file ID but different sources
4342
* (e.g. in a shared context)
44-
* @param state
4543
*/
4644
getNodesById: (state) => (fileId: number): Node[] => Object.values(state.files).filter(node => node.fileid === fileId),
4745

4846
/**
4947
* Get the root folder of a service
50-
* @param state
5148
*/
5249
getRoot: (state) => (service: Service): Folder|undefined => state.roots[service],
5350
},
@@ -115,6 +112,17 @@ export const useFilesStore = function(...args) {
115112
this.updateNodes([node])
116113
},
117114

115+
onMovedNode({ node, oldSource }: { node: Node, oldSource: string }) {
116+
if (!node.fileid) {
117+
logger.error('Trying to update/set a node without fileid', { node })
118+
return
119+
}
120+
121+
// Update the path of the node
122+
Vue.delete(this.files, oldSource)
123+
this.updateNodes([node])
124+
},
125+
118126
async onUpdatedNode(node: Node) {
119127
if (!node.fileid) {
120128
logger.error('Trying to update/set a node without fileid', { node })
@@ -147,6 +155,7 @@ export const useFilesStore = function(...args) {
147155
subscribe('files:node:created', fileStore.onCreatedNode)
148156
subscribe('files:node:deleted', fileStore.onDeletedNode)
149157
subscribe('files:node:updated', fileStore.onUpdatedNode)
158+
subscribe('files:node:moved', fileStore.onMovedNode)
150159

151160
fileStore._initialized = true
152161
}

apps/files/src/store/paths.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,40 @@ describe('Path store', () => {
127127
// See the child is removed
128128
expect(root._children).toEqual([])
129129
})
130+
131+
test('Folder is moved', () => {
132+
const node = new Folder({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/folder', id: 2 })
133+
emit('files:node:created', node)
134+
// see that the path is added and the children are set-up
135+
expect(store.paths).toEqual({ files: { [node.path]: node.source } })
136+
expect(root._children).toEqual([node.source])
137+
138+
const renamedNode = node.clone()
139+
renamedNode.rename('new-folder')
140+
141+
expect(renamedNode.path).toBe('/new-folder')
142+
expect(renamedNode.source).toBe('http://example.com/remote.php/dav/files/test/new-folder')
143+
144+
emit('files:node:moved', { node: renamedNode, oldSource: node.source })
145+
// See the path is updated
146+
expect(store.paths).toEqual({ files: { [renamedNode.path]: renamedNode.source } })
147+
// See the child is updated
148+
expect(root._children).toEqual([renamedNode.source])
149+
})
150+
151+
test('File is moved', () => {
152+
const node = new File({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/file.txt', id: 2, mime: 'text/plain' })
153+
emit('files:node:created', node)
154+
// see that the children are set-up
155+
expect(root._children).toEqual([node.source])
156+
expect(store.paths).toEqual({})
157+
158+
const renamedNode = node.clone()
159+
renamedNode.rename('new-file.txt')
160+
161+
emit('files:node:moved', { node: renamedNode, oldSource: node.source })
162+
// See the child is updated
163+
expect(root._children).toEqual([renamedNode.source])
164+
expect(store.paths).toEqual({})
165+
})
130166
})

apps/files/src/store/paths.ts

Lines changed: 65 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
*/
55
import type { FileSource, PathsStore, PathOptions, ServicesState, Service } from '../types'
66
import { defineStore } from 'pinia'
7-
import { FileType, Folder, Node, getNavigation } from '@nextcloud/files'
7+
import { File, FileType, Folder, Node, getNavigation } from '@nextcloud/files'
88
import { subscribe } from '@nextcloud/event-bus'
99
import Vue from 'vue'
1010
import logger from '../logger'
1111

1212
import { useFilesStore } from './files'
13+
import { basename, dirname } from '@nextcloud/paths'
14+
import { F } from 'lodash/fp'
1315

1416
export const usePathsStore = function(...args) {
1517
const files = useFilesStore(...args)
@@ -50,6 +52,27 @@ export const usePathsStore = function(...args) {
5052
Vue.delete(this.paths[service], path)
5153
},
5254

55+
onCreatedNode(node: Node) {
56+
const service = getNavigation()?.active?.id || 'files'
57+
if (!node.fileid) {
58+
logger.error('Node has no fileid', { node })
59+
return
60+
}
61+
62+
// Only add path if it's a folder
63+
if (node.type === FileType.Folder) {
64+
this.addPath({
65+
service,
66+
path: node.path,
67+
source: node.source,
68+
})
69+
}
70+
71+
// Update parent folder children if exists
72+
// If the folder is the root, get it and update it
73+
this.addNodeToParentChildren(node)
74+
},
75+
5376
onDeletedNode(node: Node) {
5477
const service = getNavigation()?.active?.id || 'files'
5578

@@ -61,95 +84,80 @@ export const usePathsStore = function(...args) {
6184
)
6285
}
6386

64-
// Remove node from children
65-
if (node.dirname === '/') {
66-
const root = files.getRoot(service) as Folder & { _children?: string[] }
67-
// ensure sources are unique
68-
const children = new Set(root._children ?? [])
69-
children.delete(node.source)
70-
Vue.set(root, '_children', [...children.values()])
71-
return
72-
}
73-
74-
if (this.paths[service][node.dirname]) {
75-
const parentSource = this.paths[service][node.dirname]
76-
const parentFolder = files.getNode(parentSource) as Folder & { _children?: string[] }
77-
78-
if (!parentFolder) {
79-
logger.error('Parent folder not found', { parentSource })
80-
return
81-
}
82-
83-
logger.debug('Path exists, removing from children', { parentFolder, node })
84-
85-
// ensure sources are unique
86-
const children = new Set(parentFolder._children ?? [])
87-
children.delete(node.source)
88-
Vue.set(parentFolder, '_children', [...children.values()])
89-
return
90-
}
91-
92-
logger.debug('Parent path does not exists, skipping children update', { node })
87+
this.deleteNodeFromParentChildren(node)
9388
},
9489

95-
onCreatedNode(node: Node) {
90+
onMovedNode({ node, oldSource }: { node: Node, oldSource: string }) {
9691
const service = getNavigation()?.active?.id || 'files'
97-
if (!node.fileid) {
98-
logger.error('Node has no fileid', { node })
99-
return
100-
}
10192

102-
// Only add path if it's a folder
93+
// Update the path of the node
10394
if (node.type === FileType.Folder) {
95+
// Delete the old path if it exists
96+
const oldPath = Object.entries(this.paths[service]).find(([, source]) => source === oldSource)
97+
if (oldPath?.[0]) {
98+
this.deletePath(service, oldPath[0])
99+
}
100+
101+
// Add the new path
104102
this.addPath({
105103
service,
106104
path: node.path,
107105
source: node.source,
108106
})
109107
}
110108

111-
// Update parent folder children if exists
112-
// If the folder is the root, get it and update it
113-
if (node.dirname === '/') {
114-
const root = files.getRoot(service) as Folder & { _children?: string[] }
109+
// Dummy simple clone of the renamed node from a previous state
110+
const oldNode = new File({ source: oldSource, owner: node.owner, mime: node.mime })
111+
112+
this.deleteNodeFromParentChildren(oldNode)
113+
this.addNodeToParentChildren(node)
114+
},
115+
116+
deleteNodeFromParentChildren(node: Node) {
117+
const service = getNavigation()?.active?.id || 'files'
118+
119+
// Update children of a root folder
120+
const parentSource = dirname(node.source)
121+
const folder = (node.dirname === '/' ? files.getRoot(service) : files.getNode(parentSource)) as Folder & { _children?: string[] }
122+
if (folder) {
115123
// ensure sources are unique
116-
const children = new Set(root._children ?? [])
117-
children.add(node.source)
118-
Vue.set(root, '_children', [...children.values()])
124+
const children = new Set(folder._children ?? [])
125+
children.delete(node.source)
126+
Vue.set(folder, '_children', [...children.values()])
127+
logger.debug('Children updated', { parent: folder, node, children: folder._children })
119128
return
120129
}
121130

122-
// If the folder doesn't exists yet, it will be
123-
// fetched later and its children updated anyway.
124-
if (this.paths[service][node.dirname]) {
125-
const parentSource = this.paths[service][node.dirname]
126-
const parentFolder = files.getNode(parentSource) as Folder & { _children?: string[] }
127-
logger.debug('Path already exists, updating children', { parentFolder, node })
131+
logger.debug('Parent path does not exists, skipping children update', { node })
132+
},
128133

129-
if (!parentFolder) {
130-
logger.error('Parent folder not found', { parentSource })
131-
return
132-
}
134+
addNodeToParentChildren(node: Node) {
135+
const service = getNavigation()?.active?.id || 'files'
133136

137+
// Update children of a root folder
138+
const parentSource = dirname(node.source)
139+
const folder = (node.dirname === '/' ? files.getRoot(service) : files.getNode(parentSource)) as Folder & { _children?: string[] }
140+
if (folder) {
134141
// ensure sources are unique
135-
const children = new Set(parentFolder._children ?? [])
142+
const children = new Set(folder._children ?? [])
136143
children.add(node.source)
137-
Vue.set(parentFolder, '_children', [...children.values()])
144+
Vue.set(folder, '_children', [...children.values()])
145+
logger.debug('Children updated', { parent: folder, node, children: folder._children })
138146
return
139147
}
140148

141149
logger.debug('Parent path does not exists, skipping children update', { node })
142150
},
151+
143152
},
144153
})
145154

146155
const pathsStore = store(...args)
147156
// Make sure we only register the listeners once
148157
if (!pathsStore._initialized) {
149-
// TODO: watch folders to update paths?
150158
subscribe('files:node:created', pathsStore.onCreatedNode)
151159
subscribe('files:node:deleted', pathsStore.onDeletedNode)
152-
// subscribe('files:node:moved', pathsStore.onMovedNode)
160+
subscribe('files:node:moved', pathsStore.onMovedNode)
153161

154162
pathsStore._initialized = true
155163
}

0 commit comments

Comments
 (0)