diff --git a/apps/files/src/components/BreadCrumbs.vue b/apps/files/src/components/BreadCrumbs.vue index c0048324d9102..88f592a4cdfca 100644 --- a/apps/files/src/components/BreadCrumbs.vue +++ b/apps/files/src/components/BreadCrumbs.vue @@ -54,6 +54,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', @@ -106,8 +107,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, @@ -136,19 +138,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 { @@ -156,8 +158,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) }, @@ -227,12 +229,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() } diff --git a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue index edb94e65f27a5..987b48ef8aeb8 100644 --- a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue +++ b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue @@ -24,6 +24,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', @@ -66,10 +67,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 @@ -88,20 +89,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 @@ -110,8 +111,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) diff --git a/apps/files/src/components/FileEntryMixin.ts b/apps/files/src/components/FileEntryMixin.ts index 4f6c0899910ab..c56c10de75aea 100644 --- a/apps/files/src/components/FileEntryMixin.ts +++ b/apps/files/src/components/FileEntryMixin.ts @@ -4,6 +4,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' @@ -85,13 +86,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() { @@ -116,7 +117,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) @@ -128,7 +129,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 } @@ -269,14 +270,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) @@ -330,12 +331,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() } diff --git a/apps/files/src/components/FilesListTableHeader.vue b/apps/files/src/components/FilesListTableHeader.vue index 5172202662a59..7134c957fb8c4 100644 --- a/apps/files/src/components/FilesListTableHeader.vue +++ b/apps/files/src/components/FilesListTableHeader.vue @@ -64,6 +64,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', @@ -169,7 +170,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) diff --git a/apps/files/src/components/FilesListTableHeaderActions.vue b/apps/files/src/components/FilesListTableHeaderActions.vue index 4dd868adf249d..c73cd05d01667 100644 --- a/apps/files/src/components/FilesListTableHeaderActions.vue +++ b/apps/files/src/components/FilesListTableHeaderActions.vue @@ -39,7 +39,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() @@ -64,7 +64,7 @@ export default defineComponent({ required: true, }, selectedNodes: { - type: Array as PropType, + type: Array as PropType, default: () => ([]), }, }, @@ -100,7 +100,7 @@ export default defineComponent({ nodes() { return this.selectedNodes - .map(fileid => this.getNode(fileid)) + .map(source => this.getNode(source)) .filter(Boolean) as Node[] }, @@ -144,7 +144,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 @@ -165,9 +165,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 diff --git a/apps/files/src/store/dragging.ts b/apps/files/src/store/dragging.ts index 197e18415588e..74de1c4af1699 100644 --- a/apps/files/src/store/dragging.ts +++ b/apps/files/src/store/dragging.ts @@ -4,7 +4,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: () => ({ @@ -15,7 +15,7 @@ export const useDragAndDropStore = defineStore('dragging', { /** * Set the selection of fileIds */ - set(selection = [] as FileId[]) { + set(selection = [] as FileSource[]) { Vue.set(this, 'dragging', selection) }, diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts index 36e7ad37cfd42..bf09ec7f88a56 100644 --- a/apps/files/src/store/files.ts +++ b/apps/files/src/store/files.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ 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 { defineStore } from 'pinia' import { subscribe } from '@nextcloud/event-bus' @@ -19,19 +19,20 @@ 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 the root folder of a service */ getRoot: (state) => (service: Service): Folder|undefined => state.roots[service], }, @@ -41,10 +42,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) @@ -53,8 +55,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) } }) }, diff --git a/apps/files/src/store/paths.ts b/apps/files/src/store/paths.ts index 23fd69a91e62a..2993cc9d70416 100644 --- a/apps/files/src/store/paths.ts +++ b/apps/files/src/store/paths.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -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' @@ -21,7 +21,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 } @@ -38,7 +38,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) { @@ -53,7 +53,7 @@ export const usePathsStore = function(...args) { this.addPath({ service, path: node.path, - fileid: node.fileid, + source: node.source, }) } @@ -64,26 +64,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 } diff --git a/apps/files/src/store/selection.ts b/apps/files/src/store/selection.ts index 12a265bc494e2..c8c5c6d7de313 100644 --- a/apps/files/src/store/selection.ts +++ b/apps/files/src/store/selection.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { FileId, SelectionStore } from '../types' +import type { FileSource, SelectionStore } from '../types' import { defineStore } from 'pinia' import Vue from 'vue' @@ -17,14 +17,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) diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index 3213d2c543bec..9e1ba04969705 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -7,12 +7,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 = { @@ -31,7 +31,7 @@ export interface RootOptions { // Paths store export type PathConfig = { - [path: string]: number + [path: string]: FileSource } export type ServicesState = { @@ -45,7 +45,7 @@ export type PathsStore = { export interface PathOptions { service: Service path: string - fileid: FileId + source: FileSource } // User config store @@ -57,8 +57,8 @@ export interface UserConfigStore { } export interface SelectionStore { - selected: FileId[] - lastSelection: FileId[] + selected: FileSource[] + lastSelection: FileSource[] lastSelectedIndex: number | null } @@ -92,7 +92,7 @@ export interface UploaderStore { // Drag and drop store export interface DragAndDropStore { - dragging: FileId[] + dragging: FileSource[] } export interface TemplateFile { diff --git a/apps/files/src/utils/hashUtils.ts b/apps/files/src/utils/hashUtils.ts index 6a7b55e5afef7..607064947a8be 100644 --- a/apps/files/src/utils/hashUtils.ts +++ b/apps/files/src/utils/hashUtils.ts @@ -4,8 +4,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) } diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 24797d89c3298..ae20c58ea3295 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -263,12 +263,13 @@ export default defineComponent({ if (this.dir === '/') { return this.filesStore.getRoot(this.currentView.id) } - const fileId = this.pathsStore.getPath(this.currentView.id, this.dir) - if (fileId === undefined) { + + const source = this.pathsStore.getPath(this.currentView.id, this.dir) + if (source === undefined) { return } - return this.filesStore.getNode(fileId) as Folder + return this.filesStore.getNode(source) as Folder }, /** @@ -518,7 +519,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 === '/') { @@ -527,7 +528,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.fatal('Invalid root folder returned', { dir, folder, currentView }) @@ -537,8 +538,7 @@ export default defineComponent({ // Update paths store const folders = contents.filter(node => node.type === 'folder') folders.forEach((node) => { - // Folders from API always have the fileID set - this.pathsStore.addPath({ service: currentView.id, fileid: node.fileid!, path: join(dir, node.basename) }) + this.pathsStore.addPath({ service: currentView.id, source: node.source, path: join(dir, node.basename) }) }) } catch (error) { logger.error('Error while fetching content', { error })