Skip to content

Commit 5f05cd5

Browse files
committed
fixup! feat(files): add uploader and new folder
Signed-off-by: John Molakvoæ <[email protected]>
1 parent a6a7cca commit 5f05cd5

File tree

9 files changed

+496
-50
lines changed

9 files changed

+496
-50
lines changed

apps/files/src/components/FileEntry.vue

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ import { debounce } from 'debounce'
171171
import { emit } from '@nextcloud/event-bus'
172172
import { extname } from 'path'
173173
import { generateUrl } from '@nextcloud/router'
174-
import { getFileActions, DefaultType, FileType, formatFileSize, Permission } from '@nextcloud/files'
174+
import { getFileActions, DefaultType, FileType, formatFileSize, Permission, NodeStatus } from '@nextcloud/files'
175175
import { showError, showSuccess } from '@nextcloud/dialogs'
176176
import { translate } from '@nextcloud/l10n'
177177
import { vOnClickOutside } from '@vueuse/components'
@@ -513,8 +513,10 @@ export default Vue.extend({
513513
* If renaming starts, select the file name
514514
* in the input, without the extension.
515515
*/
516-
isRenaming() {
517-
this.startRenaming()
516+
isRenaming(renaming) {
517+
if (renaming) {
518+
this.startRenaming()
519+
}
518520
},
519521
},
520522
@@ -710,9 +712,10 @@ export default Vue.extend({
710712
* input validity using browser's native validation.
711713
* @param event the keyup event
712714
*/
713-
checkInputValidity(event: KeyboardEvent) {
714-
const input = event?.target as HTMLInputElement
715+
checkInputValidity(event?: KeyboardEvent) {
716+
const input = event.target as HTMLInputElement
715717
const newName = this.newName.trim?.() || ''
718+
logger.debug('Checking input validity', { newName })
716719
try {
717720
this.isFileNameValid(newName)
718721
input.setCustomValidity('')
@@ -745,17 +748,20 @@ export default Vue.extend({
745748
},
746749
747750
startRenaming() {
748-
this.checkInputValidity()
749751
this.$nextTick(() => {
750-
const extLength = (this.source.extension || '').length
751-
const length = this.source.basename.length - extLength
752+
// Using split to get the true string length
753+
const extLength = (this.source.extension || '').split('').length
754+
const length = this.source.basename.split('').length - extLength
752755
const input = this.$refs.renameInput?.$refs?.inputField?.$refs?.input
753756
if (!input) {
754757
logger.error('Could not find the rename input')
755758
return
756759
}
757760
input.setSelectionRange(0, length)
758761
input.focus()
762+
763+
// Trigger a keyup event to update the input validity
764+
input.dispatchEvent(new Event('keyup'))
759765
})
760766
},
761767
stopRenaming() {
@@ -808,6 +814,8 @@ export default Vue.extend({
808814
emit('files:node:updated', this.source)
809815
emit('files:node:renamed', this.source)
810816
showSuccess(this.t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
817+
818+
// Reset the renaming store
811819
this.stopRenaming()
812820
this.$nextTick(() => {
813821
this.$refs.basename.focus()

apps/files/src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import './newMenu/newFolder'
1515
import Vue from 'vue'
1616
import { createPinia, PiniaVuePlugin } from 'pinia'
1717
import { getNavigation } from '@nextcloud/files'
18+
import { getRequestToken } from '@nextcloud/auth'
1819

1920
import FilesListView from './views/FilesList.vue'
2021
import NavigationView from './views/Navigation.vue'
@@ -27,6 +28,9 @@ import RouterService from './services/RouterService'
2728
import SettingsModel from './models/Setting.js'
2829
import SettingsService from './services/Settings.js'
2930

31+
// @ts-expect-error __webpack_nonce__ is injected by webpack
32+
__webpack_nonce__ = btoa(getRequestToken())
33+
3034
declare global {
3135
interface Window {
3236
OC: any;

apps/files/src/newMenu/newFolder.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,77 @@
1919
* along with this program. If not, see <http://www.gnu.org/licenses/>.
2020
*
2121
*/
22-
import { type Entry, addNewFileMenuEntry, Permission, Folder, View } from '@nextcloud/files'
22+
import type { Entry, Node } from '@nextcloud/files'
23+
24+
import { addNewFileMenuEntry, Permission, Folder } from '@nextcloud/files'
25+
import { basename, extname } from 'path'
26+
import { emit } from '@nextcloud/event-bus'
27+
import { getCurrentUser } from '@nextcloud/auth'
28+
import { showSuccess } from '@nextcloud/dialogs'
2329
import { translate as t } from '@nextcloud/l10n'
30+
import axios from '@nextcloud/axios'
2431
import FolderPlusSvg from '@mdi/svg/svg/folder-plus.svg?raw'
32+
import Vue from 'vue'
33+
34+
type createFolderResponse = {
35+
fileid: number
36+
source: string
37+
}
38+
39+
const createNewFolder = async (root: string, name: string): Promise<createFolderResponse> => {
40+
const source = root + '/' + name
41+
const response = await axios({
42+
method: 'MKCOL',
43+
url: source,
44+
headers: {
45+
Overwrite: 'F',
46+
},
47+
})
48+
return {
49+
fileid: parseInt(response.headers['oc-fileid']),
50+
source,
51+
}
52+
}
53+
54+
// TODO: move to @nextcloud/files
55+
export const getUniqueName = (name: string, names: string[]): string => {
56+
let newName = name
57+
let i = 1
58+
while (names.includes(newName)) {
59+
const ext = extname(name)
60+
newName = `${basename(name, ext)} (${i++})${ext}`
61+
}
62+
return newName
63+
}
2564

2665
const entry = {
2766
id: 'newFolder',
2867
displayName: t('files', 'New folder'),
2968
if: (context: Folder) => (context.permissions & Permission.CREATE) !== 0,
3069
iconSvgInline: FolderPlusSvg,
31-
handler(context: Folder, view: View) {
32-
console.debug(context, view)
70+
async handler(context: Folder, content: Node[]) {
71+
const contentNames = content.map((node: Node) => node.basename)
72+
const name = getUniqueName(t('files', 'New Folder'), contentNames)
73+
const { fileid, source } = await createNewFolder(context.source, name)
74+
75+
// Create the folder in the store
76+
const folder = new Folder({
77+
source,
78+
id: fileid,
79+
mtime: new Date(),
80+
owner: getCurrentUser()?.uid || null,
81+
permissions: Permission.ALL,
82+
root: context?.root || '/files/' + getCurrentUser()?.uid,
83+
})
84+
85+
if (!context._children) {
86+
Vue.set(context, '_children', [])
87+
}
88+
context._children.push(folder.fileid)
89+
90+
showSuccess(t('files', 'Created new folder "{name}"', { name: basename(source) }))
91+
emit('files:node:created', folder)
92+
emit('files:node:rename', folder)
3393
},
3494
} as Entry
3595

apps/files/src/services/Files.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import type { ContentsWithRoot } from '@nextcloud/files'
2323
import type { FileStat, ResponseDataDetailed, DAVResultResponseProps } from 'webdav'
2424

25+
import { cancelable, CancelablePromise } from 'cancelable-promise'
2526
import { File, Folder, davParsePermissions } from '@nextcloud/files'
2627
import { generateRemoteUrl } from '@nextcloud/router'
2728
import { getCurrentUser } from '@nextcloud/auth'
@@ -73,30 +74,39 @@ const resultToNode = function(node: FileStat): File | Folder {
7374
: new Folder(nodeData)
7475
}
7576

76-
export const getContents = async (path = '/'): Promise<ContentsWithRoot> => {
77+
export const getContents = (path = '/'): Promise<ContentsWithRoot> => {
78+
const controller = new AbortController()
7779
const propfindPayload = getDefaultPropfind()
7880

79-
const contentsResponse = await client.getDirectoryContents(path, {
80-
details: true,
81-
data: propfindPayload,
82-
includeSelf: true,
83-
}) as ResponseDataDetailed<FileStat[]>
81+
return new CancelablePromise(async (resolve, reject, onCancel) => {
82+
onCancel(() => controller.abort())
83+
try {
84+
const contentsResponse = await client.getDirectoryContents(path, {
85+
details: true,
86+
data: propfindPayload,
87+
includeSelf: true,
88+
signal: controller.signal,
89+
}) as ResponseDataDetailed<FileStat[]>
8490

85-
const root = contentsResponse.data[0]
86-
const contents = contentsResponse.data.slice(1)
87-
if (root.filename !== path) {
88-
throw new Error('Root node does not match requested path')
89-
}
90-
91-
return {
92-
folder: resultToNode(root) as Folder,
93-
contents: contents.map(result => {
94-
try {
95-
return resultToNode(result)
96-
} catch (error) {
97-
logger.error(`Invalid node detected '${result.basename}'`, { error })
98-
return null
91+
const root = contentsResponse.data[0]
92+
const contents = contentsResponse.data.slice(1)
93+
if (root.filename !== path) {
94+
throw new Error('Root node does not match requested path')
9995
}
100-
}).filter(Boolean) as File[],
101-
}
96+
97+
resolve({
98+
folder: resultToNode(root) as Folder,
99+
contents: contents.map(result => {
100+
try {
101+
return resultToNode(result)
102+
} catch (error) {
103+
logger.error(`Invalid node detected '${result.basename}'`, { error })
104+
return null
105+
}
106+
}).filter(Boolean) as File[],
107+
})
108+
} catch (error) {
109+
reject(error)
110+
}
111+
})
102112
}

apps/files/src/store/files.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,17 @@ export const useFilesStore = function(...args) {
8383
onDeletedNode(node: Node) {
8484
this.deleteNodes([node])
8585
},
86+
87+
onCreatedNode(node: Node) {
88+
this.updateNodes([node])
89+
},
8690
},
8791
})
8892

8993
const fileStore = store(...args)
9094
// Make sure we only register the listeners once
9195
if (!fileStore._initialized) {
92-
// subscribe('files:node:created', fileStore.onCreatedNode)
96+
subscribe('files:node:created', fileStore.onCreatedNode)
9397
subscribe('files:node:deleted', fileStore.onDeletedNode)
9498
// subscribe('files:node:moved', fileStore.onMovedNode)
9599
// subscribe('files:node:updated', fileStore.onUpdatedNode)

apps/files/src/store/paths.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
* along with this program. If not, see <http://www.gnu.org/licenses/>.
2020
*
2121
*/
22+
import { Node, getNavigation } from '@nextcloud/files'
2223
import type { FileId, PathsStore, PathOptions, ServicesState } from '../types'
2324
import { defineStore } from 'pinia'
2425
import Vue from 'vue'
26+
import logger from '../logger'
27+
import { subscribe } from '@nextcloud/event-bus'
2528

2629
export const usePathsStore = function(...args) {
2730
const store = defineStore('paths', {
@@ -50,14 +53,27 @@ export const usePathsStore = function(...args) {
5053
// Now we can set the provided path
5154
Vue.set(this.paths[payload.service], payload.path, payload.fileid)
5255
},
56+
57+
onCreatedNode(node: Node) {
58+
const currentView = getNavigation().active
59+
if (!node.fileid) {
60+
logger.error('Node has no fileid', { node })
61+
return
62+
}
63+
this.addPath({
64+
service: currentView?.id || 'files',
65+
path: node.path,
66+
fileid: node.fileid,
67+
})
68+
},
5369
},
5470
})
5571

5672
const pathsStore = store(...args)
5773
// Make sure we only register the listeners once
5874
if (!pathsStore._initialized) {
5975
// TODO: watch folders to update paths?
60-
// subscribe('files:node:created', pathsStore.onCreatedNode)
76+
subscribe('files:node:created', pathsStore.onCreatedNode)
6177
// subscribe('files:node:deleted', pathsStore.onDeletedNode)
6278
// subscribe('files:node:moved', pathsStore.onMovedNode)
6379

apps/files/src/views/FilesList.vue

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<template #actions>
2828
<!-- Uploader -->
2929
<UploadPicker v-if="currentFolder"
30+
:content="dirContents"
3031
:destination="currentFolder"
3132
:multiple="true"
3233
@uploaded="onUpload" />
@@ -72,9 +73,12 @@
7273

7374
<script lang="ts">
7475
import type { Route } from 'vue-router'
76+
import type { Upload } from '@nextcloud/upload'
7577
import type { UserConfig } from '../types.ts'
78+
import type { View, ContentsWithRoot } from '@nextcloud/files'
7679
77-
import { Folder, Node, type View, type ContentsWithRoot, join } from 'path'
80+
import { Folder, Node } from '@nextcloud/files'
81+
import { join, dirname } from 'path'
7882
import { orderBy } from 'natural-orderby'
7983
import { translate } from '@nextcloud/l10n'
8084
import { UploadPicker } from '@nextcloud/upload'
@@ -88,6 +92,7 @@ import Vue from 'vue'
8892
import { useFilesStore } from '../store/files.ts'
8993
import { usePathsStore } from '../store/paths.ts'
9094
import { useSelectionStore } from '../store/selection.ts'
95+
import { useUploaderStore } from '../store/uploader.ts'
9196
import { useUserConfigStore } from '../store/userconfig.ts'
9297
import { useViewConfigStore } from '../store/viewConfig.ts'
9398
import BreadCrumbs from '../components/BreadCrumbs.vue'
@@ -117,12 +122,14 @@ export default Vue.extend({
117122
const filesStore = useFilesStore()
118123
const pathsStore = usePathsStore()
119124
const selectionStore = useSelectionStore()
125+
const uploaderStore = useUploaderStore()
120126
const userConfigStore = useUserConfigStore()
121127
const viewConfigStore = useViewConfigStore()
122128
return {
123129
filesStore,
124130
pathsStore,
125131
selectionStore,
132+
uploaderStore,
126133
userConfigStore,
127134
viewConfigStore,
128135
}
@@ -283,6 +290,7 @@ export default Vue.extend({
283290
this.filesStore.updateNodes(contents)
284291
285292
// Define current directory children
293+
// TODO: make it more official
286294
folder._children = contents.map(node => node.fileid)
287295
288296
// If we're in the root dir, define the root
@@ -322,8 +330,22 @@ export default Vue.extend({
322330
return this.filesStore.getNode(fileId)
323331
},
324332
325-
onUpload(...args) {
326-
console.debug(args)
333+
/**
334+
* The upload manager have finished handling the queue
335+
* @param {Upload} upload the uploaded data
336+
*/
337+
onUpload(upload: Upload) {
338+
// Let's only refresh the current Folder
339+
// Navigating to a different folder will refresh it anyway
340+
const destinationSource = dirname(upload.source)
341+
const needsRefresh = destinationSource === this.currentFolder?.source
342+
343+
// TODO: fetch uploaded files data only
344+
// Use parseInt(upload.response?.headers?.['oc-fileid']) to get the fileid
345+
if (needsRefresh) {
346+
// fetchContent will cancel the previous ongoing promise
347+
this.fetchContent()
348+
}
327349
},
328350
329351
t: translate,

0 commit comments

Comments
 (0)