Merge pull request #45836 from nextcloud/backport/45251/stable28

[stable28] fix(files): do not rely on unique fileid
This commit is contained in:
John Molakvoæ 2024-06-13 10:33:43 +02:00 committed by GitHub
commit 683ae3c2fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 143 additions and 89 deletions

View file

@ -71,6 +71,7 @@ import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger'
import type { FileSource } from '../types.ts'
export default defineComponent({
name: 'BreadCrumbs',
@ -123,8 +124,9 @@ export default defineComponent({
sections() {
return this.dirs.map((dir: string, index: number) => {
const fileid = this.getFileIdFromPath(dir)
const to = { ...this.$route, params: { fileid }, query: { dir } }
const source = this.getFileSourceFromPath(dir)
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
const to = { ...this.$route, params: { node: node?.fileid }, query: { dir } }
return {
dir,
exact: true,
@ -153,19 +155,19 @@ export default defineComponent({
},
selectedFiles() {
return this.selectionStore.selected
return this.selectionStore.selected as FileSource[]
},
draggingFiles() {
return this.draggingStore.dragging
return this.draggingStore.dragging as FileSource[]
},
},
methods: {
getNodeFromId(id: number): Node | undefined {
return this.filesStore.getNode(id)
getNodeFromSource(source: FileSource): Node | undefined {
return this.filesStore.getNode(source)
},
getFileIdFromPath(path: string): number | undefined {
getFileSourceFromPath(path: string): FileSource | undefined {
return this.pathsStore.getPath(this.currentView!.id, path)
},
getDirDisplayName(path: string): string {
@ -173,8 +175,8 @@ export default defineComponent({
return this.$navigation?.active?.name || t('files', 'Home')
}
const fileId: number | undefined = this.getFileIdFromPath(path)
const node: Node | undefined = (fileId) ? this.getNodeFromId(fileId) : undefined
const source: FileSource | undefined = this.getFileSourceFromPath(path)
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
return node?.attributes?.displayName || basename(path)
},
@ -244,12 +246,12 @@ export default defineComponent({
}
// Else we're moving/copying files
const nodes = selection.map(fileid => this.filesStore.getNode(fileid)) as Node[]
const nodes = selection.map(source => this.filesStore.getNode(source)) as Node[]
await onDropInternalFiles(nodes, folder, contents.contents, isCopy)
// Reset selection after we dropped the files
// if the dropped files are within the selection
if (selection.some(fileid => this.selectedFiles.includes(fileid))) {
if (selection.some(source => this.selectedFiles.includes(source))) {
logger.debug('Dropped selection, resetting select store...')
this.selectionStore.reset()
}

View file

@ -41,6 +41,7 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import { useKeyboardStore } from '../../store/keyboard.ts'
import { useSelectionStore } from '../../store/selection.ts'
import logger from '../../logger.js'
import type { FileSource } from '../../types.ts'
export default defineComponent({
name: 'FileEntryCheckbox',
@ -83,10 +84,10 @@ export default defineComponent({
return this.selectionStore.selected
},
isSelected() {
return this.selectedFiles.includes(this.fileid)
return this.selectedFiles.includes(this.source.source)
},
index() {
return this.nodes.findIndex((node: Node) => node.fileid === this.fileid)
return this.nodes.findIndex((node: Node) => node.source === this.source.source)
},
isFile() {
return this.source.type === FileType.File
@ -105,20 +106,20 @@ export default defineComponent({
// Get the last selected and select all files in between
if (this.keyboardStore?.shiftKey && lastSelectedIndex !== null) {
const isAlreadySelected = this.selectedFiles.includes(this.fileid)
const isAlreadySelected = this.selectedFiles.includes(this.source.source)
const start = Math.min(newSelectedIndex, lastSelectedIndex)
const end = Math.max(lastSelectedIndex, newSelectedIndex)
const lastSelection = this.selectionStore.lastSelection
const filesToSelect = this.nodes
.map(file => file.fileid)
.map(file => file.source)
.slice(start, end + 1)
.filter(Boolean) as number[]
.filter(Boolean) as FileSource[]
// If already selected, update the new selection _without_ the current file
const selection = [...lastSelection, ...filesToSelect]
.filter(fileid => !isAlreadySelected || fileid !== this.fileid)
.filter(source => !isAlreadySelected || source !== this.source.source)
logger.debug('Shift key pressed, selecting all files in between', { start, end, filesToSelect, isAlreadySelected })
// Keep previous lastSelectedIndex to be use for further shift selections
@ -127,8 +128,8 @@ export default defineComponent({
}
const selection = selected
? [...this.selectedFiles, this.fileid]
: this.selectedFiles.filter(fileid => fileid !== this.fileid)
? [...this.selectedFiles, this.source.source]
: this.selectedFiles.filter(source => source !== this.source.source)
logger.debug('Updating selection', { selection })
this.selectionStore.set(selection)

View file

@ -21,6 +21,7 @@
*/
import type { ComponentPublicInstance, PropType } from 'vue'
import type { FileSource } from '../types.ts'
import { showError } from '@nextcloud/dialogs'
import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, View } from '@nextcloud/files'
@ -102,13 +103,13 @@ export default defineComponent({
},
draggingFiles() {
return this.draggingStore.dragging
return this.draggingStore.dragging as FileSource[]
},
selectedFiles() {
return this.selectionStore.selected
return this.selectionStore.selected as FileSource[]
},
isSelected() {
return this.fileid && this.selectedFiles.includes(this.fileid)
return this.selectedFiles.includes(this.source.source)
},
isRenaming() {
@ -133,7 +134,7 @@ export default defineComponent({
// If we're dragging a selection, we need to check all files
if (this.selectedFiles.length > 0) {
const nodes = this.selectedFiles.map(fileid => this.filesStore.getNode(fileid)) as Node[]
const nodes = this.selectedFiles.map(source => this.filesStore.getNode(source)) as Node[]
return nodes.every(canDrag)
}
return canDrag(this.source)
@ -145,7 +146,7 @@ export default defineComponent({
}
// If the current folder is also being dragged, we can't drop it on itself
if (this.fileid && this.draggingFiles.includes(this.fileid)) {
if (this.draggingFiles.includes(this.source.source)) {
return false
}
@ -286,14 +287,14 @@ export default defineComponent({
// Dragging set of files, if we're dragging a file
// that is already selected, we use the entire selection
if (this.selectedFiles.includes(this.fileid)) {
if (this.selectedFiles.includes(this.source.source)) {
this.draggingStore.set(this.selectedFiles)
} else {
this.draggingStore.set([this.fileid])
this.draggingStore.set([this.source.source])
}
const nodes = this.draggingStore.dragging
.map(fileid => this.filesStore.getNode(fileid)) as Node[]
.map(source => this.filesStore.getNode(source)) as Node[]
const image = await getDragAndDropPreview(nodes)
event.dataTransfer?.setDragImage(image, -10, -10)
@ -347,12 +348,12 @@ export default defineComponent({
}
// Else we're moving/copying files
const nodes = selection.map(fileid => this.filesStore.getNode(fileid)) as Node[]
const nodes = selection.map(source => this.filesStore.getNode(source)) as Node[]
await onDropInternalFiles(nodes, folder, contents.contents, isCopy)
// Reset selection after we dropped the files
// if the dropped files are within the selection
if (selection.some(fileid => this.selectedFiles.includes(fileid))) {
if (selection.some(source => this.selectedFiles.includes(source))) {
logger.debug('Dropped selection, resetting select store...')
this.selectionStore.reset()
}

View file

@ -81,6 +81,7 @@ import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
import filesSortingMixin from '../mixins/filesSorting.ts'
import logger from '../logger.js'
import type { Node } from '@nextcloud/files'
import type { FileSource } from '../types.ts'
export default defineComponent({
name: 'FilesListTableHeader',
@ -186,7 +187,7 @@ export default defineComponent({
onToggleAll(selected) {
if (selected) {
const selection = this.nodes.map(node => node.fileid).filter(Boolean) as number[]
const selection = this.nodes.map(node => node.source).filter(Boolean) as FileSource[]
logger.debug('Added all nodes to selection', { selection })
this.selectionStore.setLastIndex(null)
this.selectionStore.set(selection)

View file

@ -56,7 +56,7 @@ import { useFilesStore } from '../store/files.ts'
import { useSelectionStore } from '../store/selection.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger.js'
import type { FileId } from '../types'
import type { FileSource } from '../types'
// The registered actions list
const actions = getFileActions()
@ -81,7 +81,7 @@ export default defineComponent({
required: true,
},
selectedNodes: {
type: Array as PropType<FileId[]>,
type: Array as PropType<FileSource[]>,
default: () => ([]),
},
},
@ -117,7 +117,7 @@ export default defineComponent({
nodes() {
return this.selectedNodes
.map(fileid => this.getNode(fileid))
.map(source => this.getNode(source))
.filter(Boolean) as Node[]
},
@ -161,7 +161,7 @@ export default defineComponent({
async onActionClick(action) {
const displayName = action.displayName(this.nodes, this.currentView)
const selectionIds = this.selectedNodes
const selectionSources = this.selectedNodes
try {
// Set loading markers
this.loading = action.id
@ -182,9 +182,9 @@ export default defineComponent({
// Handle potential failures
if (results.some(result => result === false)) {
// Remove the failed ids from the selection
const failedIds = selectionIds
.filter((fileid, index) => results[index] === false)
this.selectionStore.set(failedIds)
const failedSources = selectionSources
.filter((source, index) => results[index] === false)
this.selectionStore.set(failedSources)
if (results.some(result => result === null)) {
// If some actions returned null, we assume that the dev

View file

@ -21,7 +21,7 @@
*/
import { defineStore } from 'pinia'
import Vue from 'vue'
import type { FileId, DragAndDropStore } from '../types'
import type { DragAndDropStore, FileSource } from '../types'
export const useDragAndDropStore = defineStore('dragging', {
state: () => ({
@ -32,7 +32,7 @@ export const useDragAndDropStore = defineStore('dragging', {
/**
* Set the selection of fileIds
*/
set(selection = [] as FileId[]) {
set(selection = [] as FileSource[]) {
Vue.set(this, 'dragging', selection)
},

View file

@ -19,14 +19,28 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Folder, Node } from '@nextcloud/files'
import type { FilesStore, RootsStore, RootOptions, Service, FilesState, FileId } from '../types'
import type { FilesStore, RootsStore, RootOptions, Service, FilesState, FileSource } from '../types'
import type { FileStat, ResponseDataDetailed } from 'webdav'
import type { Folder, Node } from '@nextcloud/files'
import { davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import { defineStore } from 'pinia'
import { subscribe } from '@nextcloud/event-bus'
import logger from '../logger'
import Vue from 'vue'
import { client } from '../services/WebdavClient.ts'
const fetchNode = async (node: Node): Promise<Node> => {
const propfindPayload = davGetDefaultPropfind()
const result = await client.stat(`${davRootPath}${node.path}`, {
details: true,
data: propfindPayload,
}) as ResponseDataDetailed<FileStat>
return davResultToNode(result.data)
}
export const useFilesStore = function(...args) {
const store = defineStore('files', {
state: (): FilesState => ({
@ -36,19 +50,27 @@ export const useFilesStore = function(...args) {
getters: {
/**
* Get a file or folder by id
* Get a file or folder by its source
*/
getNode: (state) => (id: FileId): Node|undefined => state.files[id],
getNode: (state) => (source: FileSource): Node|undefined => state.files[source],
/**
* Get a list of files or folders by their IDs
* Does not return undefined values
* Note: does not return undefined values
*/
getNodes: (state) => (ids: FileId[]): Node[] => ids
.map(id => state.files[id])
getNodes: (state) => (sources: FileSource[]): Node[] => sources
.map(source => state.files[source])
.filter(Boolean),
/**
* Get a file or folder by id
* Get files or folders by their file ID
* Multiple nodes can have the same file ID but different sources
* (e.g. in a shared context)
*/
getNodesById: (state) => (fileId: number): Node[] => Object.values(state.files).filter(node => node.fileid === fileId),
/**
* Get the root folder of a service
*/
getRoot: (state) => (service: Service): Folder|undefined => state.roots[service],
},
@ -58,10 +80,11 @@ export const useFilesStore = function(...args) {
// Update the store all at once
const files = nodes.reduce((acc, node) => {
if (!node.fileid) {
logger.error('Trying to update/set a node without fileid', node)
logger.error('Trying to update/set a node without fileid', { node })
return acc
}
acc[node.fileid] = node
acc[node.source] = node
return acc
}, {} as FilesStore)
@ -70,8 +93,8 @@ export const useFilesStore = function(...args) {
deleteNodes(nodes: Node[]) {
nodes.forEach(node => {
if (node.fileid) {
Vue.delete(this.files, node.fileid)
if (node.source) {
Vue.delete(this.files, node.source)
}
})
},
@ -88,8 +111,28 @@ export const useFilesStore = function(...args) {
this.updateNodes([node])
},
onUpdatedNode(node: Node) {
this.updateNodes([node])
async onUpdatedNode(node: Node) {
if (!node.fileid) {
logger.error('Trying to update/set a node without fileid', { node })
return
}
// If we have multiple nodes with the same file ID, we need to update all of them
const nodes = this.getNodesById(node.fileid)
if (nodes.length > 1) {
await Promise.all(nodes.map(fetchNode)).then(this.updateNodes)
logger.debug(nodes.length + ' nodes updated in store', { fileid: node.fileid })
return
}
// If we have only one node with the file ID, we can update it directly
if (node.source === nodes[0].source) {
this.updateNodes([node])
return
}
// Otherwise, it means we receive an event for a node that is not in the store
fetchNode(node).then(n => this.updateNodes([n]))
},
},
})

View file

@ -19,7 +19,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { FileId, PathsStore, PathOptions, ServicesState } from '../types'
import type { FileSource, PathsStore, PathOptions, ServicesState } from '../types'
import { defineStore } from 'pinia'
import { FileType, Folder, Node, getNavigation } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
@ -38,7 +38,7 @@ export const usePathsStore = function(...args) {
getters: {
getPath: (state) => {
return (service: string, path: string): FileId|undefined => {
return (service: string, path: string): FileSource|undefined => {
if (!state.paths[service]) {
return undefined
}
@ -55,7 +55,7 @@ export const usePathsStore = function(...args) {
}
// Now we can set the provided path
Vue.set(this.paths[payload.service], payload.path, payload.fileid)
Vue.set(this.paths[payload.service], payload.path, payload.source)
},
onCreatedNode(node: Node) {
@ -70,7 +70,7 @@ export const usePathsStore = function(...args) {
this.addPath({
service,
path: node.path,
fileid: node.fileid,
source: node.source,
})
}
@ -81,26 +81,26 @@ export const usePathsStore = function(...args) {
if (!root._children) {
Vue.set(root, '_children', [])
}
root._children.push(node.fileid)
root._children.push(node.source)
return
}
// If the folder doesn't exists yet, it will be
// fetched later and its children updated anyway.
if (this.paths[service][node.dirname]) {
const parentId = this.paths[service][node.dirname]
const parentFolder = files.getNode(parentId) as Folder
const parentSource = this.paths[service][node.dirname]
const parentFolder = files.getNode(parentSource) as Folder
logger.debug('Path already exists, updating children', { parentFolder, node })
if (!parentFolder) {
logger.error('Parent folder not found', { parentId })
logger.error('Parent folder not found', { parentSource })
return
}
if (!parentFolder._children) {
Vue.set(parentFolder, '_children', [])
}
parentFolder._children.push(node.fileid)
parentFolder._children.push(node.source)
return
}

View file

@ -21,7 +21,7 @@
*/
import { defineStore } from 'pinia'
import Vue from 'vue'
import { FileId, SelectionStore } from '../types'
import { FileSource, SelectionStore } from '../types'
export const useSelectionStore = defineStore('selection', {
state: () => ({
@ -34,14 +34,14 @@ export const useSelectionStore = defineStore('selection', {
/**
* Set the selection of fileIds
*/
set(selection = [] as FileId[]) {
set(selection = [] as FileSource[]) {
Vue.set(this, 'selected', [...new Set(selection)])
},
/**
* Set the last selected index
*/
setLastIndex(lastSelectedIndex = null as FileId | null) {
setLastIndex(lastSelectedIndex = null as number | null) {
// Update the last selection if we provided a new selection starting point
Vue.set(this, 'lastSelection', lastSelectedIndex ? this.selected : [])
Vue.set(this, 'lastSelectedIndex', lastSelectedIndex)

View file

@ -24,12 +24,12 @@ import type { Upload } from '@nextcloud/upload'
// Global definitions
export type Service = string
export type FileId = number
export type FileSource = string
export type ViewId = string
// Files store
export type FilesStore = {
[fileid: FileId]: Node
[source: FileSource]: Node
}
export type RootsStore = {
@ -48,7 +48,7 @@ export interface RootOptions {
// Paths store
export type PathConfig = {
[path: string]: number
[path: string]: FileSource
}
export type ServicesState = {
@ -62,7 +62,7 @@ export type PathsStore = {
export interface PathOptions {
service: Service
path: string
fileid: FileId
source: FileSource
}
// User config store
@ -74,8 +74,8 @@ export interface UserConfigStore {
}
export interface SelectionStore {
selected: FileId[]
lastSelection: FileId[]
selected: FileSource[]
lastSelection: FileSource[]
lastSelectedIndex: number | null
}
@ -109,7 +109,7 @@ export interface UploaderStore {
// Drag and drop store
export interface DragAndDropStore {
dragging: FileId[]
dragging: FileSource[]
}
export interface TemplateFile {

View file

@ -21,8 +21,9 @@
*/
export const hashCode = function(str: string): number {
return str.split('').reduce(function(a, b) {
a = ((a << 5) - a) + b.charCodeAt(0)
return a & a
}, 0)
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0
}
return (hash >>> 0)
}

View file

@ -259,8 +259,13 @@ export default defineComponent({
if (this.dir === '/') {
return this.filesStore.getRoot(this.currentView.id)
}
const fileId = this.pathsStore.getPath(this.currentView.id, this.dir)
return this.filesStore.getNode(fileId)
const source = this.pathsStore.getPath(this.currentView.id, this.dir)
if (source === undefined) {
return
}
return this.filesStore.getNode(source) as Folder
},
/**
@ -507,7 +512,7 @@ export default defineComponent({
// Define current directory children
// TODO: make it more official
this.$set(folder, '_children', contents.map(node => node.fileid))
this.$set(folder, '_children', contents.map(node => node.source))
// If we're in the root dir, define the root
if (dir === '/') {
@ -516,7 +521,7 @@ export default defineComponent({
// Otherwise, add the folder to the store
if (folder.fileid) {
this.filesStore.updateNodes([folder])
this.pathsStore.addPath({ service: currentView.id, fileid: folder.fileid, path: dir })
this.pathsStore.addPath({ service: currentView.id, source: folder.source, path: dir })
} else {
// If we're here, the view API messed up
logger.error('Invalid root folder returned', { dir, folder, currentView })

4
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long