diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsAcceptShare.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsAcceptShare.ts index 3da81c08f43..eb3c26fe061 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsAcceptShare.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsAcceptShare.ts @@ -8,6 +8,7 @@ import { useCapabilityFilesSharingResharing, useCapabilityShareJailEnabled, useClientService, + useLoadingService, useRouter, useStore } from 'web-pkg/src/composables' @@ -23,6 +24,7 @@ export const useFileActionsAcceptShare = ({ store }: { store?: Store } = {} const hasResharing = useCapabilityFilesSharingResharing() const hasShareJail = useCapabilityShareJailEnabled() const { owncloudSdk } = useClientService() + const loadingService = useLoadingService() const handler = async ({ resources }) => { const errors = [] @@ -81,7 +83,7 @@ export const useFileActionsAcceptShare = ({ store }: { store?: Store } = {} { name: 'accept-share', icon: 'check', - handler, + handler: (args) => loadingService.addTask(() => handler(args)), label: ({ resources }) => $ngettext('Accept share', 'Accept shares', resources.length), isEnabled: ({ space, resources }) => { if ( diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsDeclineShare.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsDeclineShare.ts index 046a19cac3f..329bb36203a 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsDeclineShare.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsDeclineShare.ts @@ -11,6 +11,7 @@ import { useCapabilityFilesSharingResharing, useCapabilityShareJailEnabled, useClientService, + useLoadingService, useRouter, useStore } from 'web-pkg/src/composables' @@ -26,6 +27,7 @@ export const useFileActionsDeclineShare = ({ store }: { store?: Store } = { const hasResharing = useCapabilityFilesSharingResharing() const hasShareJail = useCapabilityShareJailEnabled() const { owncloudSdk } = useClientService() + const loadingService = useLoadingService() const handler = async ({ resources }) => { const errors = [] @@ -85,7 +87,7 @@ export const useFileActionsDeclineShare = ({ store }: { store?: Store } = { { name: 'decline-share', icon: 'spam-3', - handler, + handler: (args) => loadingService.addTask(() => handler(args)), label: ({ resources }) => $ngettext('Decline share', 'Decline shares', resources.length), isEnabled: ({ space, resources }) => { if ( diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsDownloadArchive.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsDownloadArchive.ts index 87c207c934c..be37ae6dfdc 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsDownloadArchive.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsDownloadArchive.ts @@ -11,13 +11,19 @@ import { archiverService } from '../../../services' import { isPublicSpaceResource, Resource } from 'web-client/src/helpers' import { Store } from 'vuex' import { computed, unref } from 'vue' -import { usePublicLinkPassword, useRouter, useStore } from 'web-pkg/src/composables' +import { + useLoadingService, + usePublicLinkPassword, + useRouter, + useStore +} from 'web-pkg/src/composables' import { Action, ActionOptions } from 'web-pkg/src/composables/actions' import { useGettext } from 'vue3-gettext' export const useFileActionsDownloadArchive = ({ store }: { store?: Store } = {}) => { store = store || useStore() const router = useRouter() + const loadingService = useLoadingService() const { $ngettext, $gettext } = useGettext() const publicLinkPassword = usePublicLinkPassword({ store }) const isFilesAppActive = useIsFilesAppActive() @@ -31,25 +37,27 @@ export const useFileActionsDownloadArchive = ({ store }: { store?: Store } dir: path.dirname(first(resources).path) || '/', files: resources.map((resource) => resource.name) } - await archiverService - .triggerDownload({ - ...fileOptions, - ...(isPublicSpaceResource(space) && { - publicToken: space.id as string, - publicLinkPassword: unref(publicLinkPassword) + return loadingService.addTask(() => { + return archiverService + .triggerDownload({ + ...fileOptions, + ...(isPublicSpaceResource(space) && { + publicToken: space.id as string, + publicLinkPassword: unref(publicLinkPassword) + }) }) - }) - .catch((e) => { - console.error(e) - store.dispatch('showMessage', { - title: $ngettext( - 'Failed to download the selected folder.', // on single selection only available for folders - 'Failed to download the selected files.', // on multi selection available for files+folders - resources.length - ), - status: 'danger' + .catch((e) => { + console.error(e) + store.dispatch('showMessage', { + title: $ngettext( + 'Failed to download the selected folder.', // on single selection only available for folders + 'Failed to download the selected files.', // on multi selection available for files+folders + resources.length + ), + status: 'danger' + }) }) - }) + }) } const actions = computed((): Action[] => { diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsEmptyTrashBin.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsEmptyTrashBin.ts index 37cb255391d..c0255a7b72c 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsEmptyTrashBin.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsEmptyTrashBin.ts @@ -8,6 +8,7 @@ import { useCapabilityFilesPermanentDeletion, useCapabilityShareJailEnabled, useClientService, + useLoadingService, useRouter, useStore } from 'web-pkg/src/composables' @@ -19,6 +20,7 @@ export const useFileActionsEmptyTrashBin = ({ store }: { store?: Store } = const router = useRouter() const { $gettext, $pgettext } = useGettext() const { owncloudSdk } = useClientService() + const loadingService = useLoadingService() const hasShareJail = useCapabilityShareJailEnabled() const hasPermanentDeletion = useCapabilityFilesPermanentDeletion() @@ -27,27 +29,29 @@ export const useFileActionsEmptyTrashBin = ({ store }: { store?: Store } = ? buildWebDavSpacesTrashPath(space.id) : buildWebDavFilesTrashPath(store.getters.user.id) - return owncloudSdk.fileTrash - .clearTrashBin(path) - .then(() => { - store.dispatch('showMessage', { - title: $gettext('All deleted files were removed') + return loadingService.addTask(() => { + return owncloudSdk.fileTrash + .clearTrashBin(path) + .then(() => { + store.dispatch('showMessage', { + title: $gettext('All deleted files were removed') + }) + store.dispatch('Files/clearTrashBin') }) - store.dispatch('Files/clearTrashBin') - }) - .catch((error) => { - console.error(error) - store.dispatch('showMessage', { - title: $pgettext( - 'Error message in case clearing the trash bin fails', - 'Failed to delete all files permanently' - ), - status: 'danger' + .catch((error) => { + console.error(error) + store.dispatch('showMessage', { + title: $pgettext( + 'Error message in case clearing the trash bin fails', + 'Failed to delete all files permanently' + ), + status: 'danger' + }) }) - }) - .finally(() => { - store.dispatch('hideModal') - }) + .finally(() => { + store.dispatch('hideModal') + }) + }) } const handler = ({ space }: ActionOptions) => { diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsPaste.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsPaste.ts index 76ab69d86d7..094a0ca08bd 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsPaste.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsPaste.ts @@ -6,13 +6,14 @@ import { import { Store } from 'vuex' import { computed, unref } from 'vue' import { useGettext } from 'vue3-gettext' -import { useClientService, useRouter, useStore } from 'web-pkg/src/composables' +import { useClientService, useLoadingService, useRouter, useStore } from 'web-pkg/src/composables' import { Action } from 'web-pkg/src/composables/actions' export const useFileActionsPaste = ({ store }: { store?: Store } = {}) => { store = store || useStore() const router = useRouter() const clientService = useClientService() + const loadingService = useLoadingService() const { $gettext, $pgettext, interpolate: $gettextInterpolate, $ngettext } = useGettext() const isMacOs = computed(() => { @@ -30,6 +31,7 @@ export const useFileActionsPaste = ({ store }: { store?: Store } = {}) => { store.dispatch('Files/pasteSelectedFiles', { targetSpace: space, clientService, + loadingService, createModal: (...args) => store.dispatch('createModal', ...args), hideModal: (...args) => store.dispatch('hideModal', ...args), showMessage: (...args) => store.dispatch('showMessage', ...args), diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsRename.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsRename.ts index ab28a2bea67..a6f3f78b6ea 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsRename.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsRename.ts @@ -12,7 +12,7 @@ import { import { createFileRouteOptions } from 'web-pkg/src/helpers/router' import { renameResource as _renameResource } from '../../../helpers/resources' import { computed, unref } from 'vue' -import { useClientService, useRouter, useStore } from 'web-pkg/src/composables' +import { useClientService, useLoadingService, useRouter, useStore } from 'web-pkg/src/composables' import { useGettext } from 'vue3-gettext' import { Action, ActionOptions } from 'web-pkg/src/composables/actions' import { useCapabilityFilesSharingCanRename } from 'web-pkg/src/composables/capability' @@ -22,6 +22,7 @@ export const useFileActionsRename = ({ store }: { store?: Store } = {}) => const router = useRouter() const { $gettext, interpolate: $gettextInterpolate } = useGettext() const clientService = useClientService() + const loadingService = useLoadingService() const canRename = useCapabilityFilesSharingCanRename() const checkNewName = (resource, newName, parentResources = undefined) => { @@ -167,7 +168,9 @@ export const useFileActionsRename = ({ store }: { store?: Store } = {}) => newName = `${newName}.${resources[0].extension}` } - renameResource(space, resources[0], newName) + return loadingService.addTask(() => { + return renameResource(space, resources[0], newName) + }) } const checkName = (newName) => { if (!areFileExtensionsShown) { diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsRestore.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsRestore.ts index cd6e4f019ec..deec13d8342 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsRestore.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsRestore.ts @@ -19,12 +19,14 @@ import { useAccessToken, useCapabilitySpacesEnabled, useClientService, + useLoadingService, useRouter, useStore } from 'web-pkg/src/composables' import { computed, unref } from 'vue' import { useGettext } from 'vue3-gettext' import { Action, ActionOptions } from 'web-pkg/src/composables/actions' +import { LoadingTaskCallbackArguments } from 'web-pkg' export const useFileActionsRestore = ({ store }: { store?: Store } = {}) => { store = store || useStore() @@ -32,6 +34,7 @@ export const useFileActionsRestore = ({ store }: { store?: Store } = {}) => const { $gettext, $ngettext, interpolate: $gettextInterpolate } = useGettext() const clientService = useClientService() const accessToken = useAccessToken({ store }) + const loadingService = useLoadingService() const hasSpacesEnabled = useCapabilitySpacesEnabled() @@ -151,13 +154,14 @@ export const useFileActionsRestore = ({ store }: { store?: Store } = {}) => const restoreResources = async ( space: SpaceResource, resources: Resource[], - missingFolderPaths: string[] + missingFolderPaths: string[], + { setProgress }: LoadingTaskCallbackArguments ) => { const restoredResources = [] const failedResources = [] let createdFolderPaths = [] - for (const resource of resources) { + for (const [i, resource] of resources.entries()) { const parentPath = dirname(resource.path) if (missingFolderPaths.includes(parentPath)) { const { existingPaths } = await createFolderStructure(space, parentPath, createdFolderPaths) @@ -172,6 +176,8 @@ export const useFileActionsRestore = ({ store }: { store?: Store } = {}) => } catch (e) { console.error(e) failedResources.push(resource) + } finally { + setProgress({ total: resources.length, current: i + 1 }) } } @@ -261,7 +267,12 @@ export const useFileActionsRestore = ({ store }: { store?: Store } = {}) => resource.path = urlJoin(parentPath, resolvedName) resolvedResources.push(resource) } - return restoreResources(space, resolvedResources, missingFolderPaths) + return loadingService.addTask( + ({ setProgress }) => { + return restoreResources(space, resolvedResources, missingFolderPaths, { setProgress }) + }, + { indeterminate: false } + ) } const actions = computed((): Action[] => [ diff --git a/packages/web-app-files/src/composables/actions/helpers/useFileActionsDeleteResources.ts b/packages/web-app-files/src/composables/actions/helpers/useFileActionsDeleteResources.ts index 5d7deee9411..9020675e014 100644 --- a/packages/web-app-files/src/composables/actions/helpers/useFileActionsDeleteResources.ts +++ b/packages/web-app-files/src/composables/actions/helpers/useFileActionsDeleteResources.ts @@ -14,7 +14,8 @@ import { useCapabilityShareJailEnabled, useClientService, useRouter, - useStore + useStore, + useLoadingService } from 'web-pkg/src/composables' import { useGettext } from 'vue3-gettext' import { ref } from 'vue' @@ -27,6 +28,7 @@ export const useFileActionsDeleteResources = ({ store }: { store?: Store }) const hasShareJail = useCapabilityShareJailEnabled() const hasSpacesEnabled = useCapabilitySpacesEnabled() const clientService = useClientService() + const loadingService = useLoadingService() const { owncloudSdk } = clientService const accessToken = useAccessToken({ store }) @@ -143,6 +145,7 @@ export const useFileActionsDeleteResources = ({ store }: { store?: Store }) const trashbin_delete = (space: SpaceResource) => { // TODO: use clear all if all files are selected store.dispatch('toggleModalConfirmButton') + // FIXME: Implement proper batch delete and add loading indicator for (const file of unref(resources)) { const p = queue.add(() => { return trashbin_deleteOp(space, file) @@ -157,51 +160,59 @@ export const useFileActionsDeleteResources = ({ store }: { store?: Store }) } const filesList_delete = (space: SpaceResource) => { - store - .dispatch('Files/deleteFiles', { - ...language, - space, - files: unref(resources), - clientService - }) - .then(async () => { - store.dispatch('hideModal') - store.dispatch('toggleModalConfirmButton') - - // Load quota - if ( - isLocationSpacesActive(router, 'files-spaces-generic') && - !['public', 'share'].includes(space?.driveType) - ) { - if (unref(hasSpacesEnabled)) { - const graphClient = clientService.graphAuthenticated( - store.getters.configuration.server, - unref(accessToken) - ) - const driveResponse = await graphClient.drives.getDrive(unref(resources)[0].storageId) - store.commit('runtime/spaces/UPDATE_SPACE_FIELD', { - id: driveResponse.data.id, - field: 'spaceQuota', - value: driveResponse.data.quota - }) - } else { - const user = await owncloudSdk.users.getUser(store.getters.user.id) - store.commit('SET_QUOTA', user.quota) - } - } - - if ( - unref(resourcesToDelete).length && - isSameResource(unref(resourcesToDelete)[0], store.getters['Files/currentFolder']) - ) { - return router.push( - createFileRouteOptions(space, { - path: dirname(unref(resourcesToDelete)[0].path), - fileId: unref(resourcesToDelete)[0].parentFolderId - }) - ) - } - }) + return loadingService.addTask( + (loadingCallbackArgs) => { + return store + .dispatch('Files/deleteFiles', { + ...language, + space, + files: unref(resources), + clientService, + loadingCallbackArgs + }) + .then(async () => { + store.dispatch('hideModal') + store.dispatch('toggleModalConfirmButton') + + // Load quota + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + !['public', 'share'].includes(space?.driveType) + ) { + if (unref(hasSpacesEnabled)) { + const graphClient = clientService.graphAuthenticated( + store.getters.configuration.server, + unref(accessToken) + ) + const driveResponse = await graphClient.drives.getDrive( + unref(resources)[0].storageId + ) + store.commit('runtime/spaces/UPDATE_SPACE_FIELD', { + id: driveResponse.data.id, + field: 'spaceQuota', + value: driveResponse.data.quota + }) + } else { + const user = await owncloudSdk.users.getUser(store.getters.user.id) + store.commit('SET_QUOTA', user.quota) + } + } + + if ( + unref(resourcesToDelete).length && + isSameResource(unref(resourcesToDelete)[0], store.getters['Files/currentFolder']) + ) { + return router.push( + createFileRouteOptions(space, { + path: dirname(unref(resourcesToDelete)[0].path), + fileId: unref(resourcesToDelete)[0].parentFolderId + }) + ) + } + }) + }, + { indeterminate: false } + ) } const deleteHelper = (space: SpaceResource) => { diff --git a/packages/web-app-files/src/composables/actions/spaces/useSpaceActionsSetImage.ts b/packages/web-app-files/src/composables/actions/spaces/useSpaceActionsSetImage.ts index 9bfc9db591b..6008d2469c7 100644 --- a/packages/web-app-files/src/composables/actions/spaces/useSpaceActionsSetImage.ts +++ b/packages/web-app-files/src/composables/actions/spaces/useSpaceActionsSetImage.ts @@ -1,7 +1,7 @@ import { isLocationSpacesActive } from '../../../router' import { Store } from 'vuex' import { thumbnailService } from '../../../services' -import { useClientService, useRouter, useStore } from 'web-pkg/src/composables' +import { useClientService, useLoadingService, useRouter, useStore } from 'web-pkg/src/composables' import { useGettext } from 'vue3-gettext' import { computed } from 'vue' import { ActionOptions } from 'web-pkg/src/composables/actions' @@ -11,6 +11,7 @@ export const useFileActionsSetImage = ({ store }: { store?: Store } = {}) = const router = useRouter() const { $gettext } = useGettext() const clientService = useClientService() + const loadingService = useLoadingService() const handler = async ({ space, resources }: ActionOptions) => { const accessToken = store.getters['runtime/auth/accessToken'] @@ -72,7 +73,7 @@ export const useFileActionsSetImage = ({ store }: { store?: Store } = {}) = { name: 'set-space-image', icon: 'image-edit', - handler, + handler: (args) => loadingService.addTask(() => handler(args)), label: () => { return $gettext('Set as space image') }, diff --git a/packages/web-app-files/src/composables/actions/spaces/useSpaceActionsUploadImage.ts b/packages/web-app-files/src/composables/actions/spaces/useSpaceActionsUploadImage.ts index 2f55900bf03..6610e8e0889 100644 --- a/packages/web-app-files/src/composables/actions/spaces/useSpaceActionsUploadImage.ts +++ b/packages/web-app-files/src/composables/actions/spaces/useSpaceActionsUploadImage.ts @@ -2,7 +2,7 @@ import { computed, unref, VNodeRef } from 'vue' import { Store } from 'vuex' import { SpaceResource } from 'web-client/src' import { Drive } from 'web-client/src/generated' -import { useClientService, useStore } from 'web-pkg/src/composables' +import { useClientService, useLoadingService, useStore } from 'web-pkg/src/composables' import { eventBus } from 'web-pkg/src/services/eventBus' import { thumbnailService } from '../../../services' import { useGettext } from 'vue3-gettext' @@ -18,6 +18,7 @@ export const useSpaceActionsUploadImage = ({ store = store || useStore() const { $gettext } = useGettext() const clientService = useClientService() + const loadingService = useLoadingService() let selectedSpace: SpaceResource = null const handler = ({ resources }: ActionOptions) => { @@ -55,46 +56,48 @@ export const useSpaceActionsUploadImage = ({ extraHeaders['X-OC-Mtime'] = '' + file.lastModified / 1000 } - return clientService.owncloudSdk.files - .putFileContents(`/spaces/${selectedSpace.id}/.space/${file.name}`, file, { - headers: extraHeaders, - overwrite: true - }) - .then((image) => { - return graphClient.drives - .updateDrive( - selectedSpace.id as string, - { - special: [ - { - specialFolder: { - name: 'image' - }, - id: image['OC-FileId'] - } - ] - } as Drive, - {} - ) - .then(({ data }) => { - store.commit('runtime/spaces/UPDATE_SPACE_FIELD', { - id: selectedSpace.id as string, - field: 'spaceImageData', - value: data.special.find((special) => special.specialFolder.name === 'image') - }) - store.dispatch('showMessage', { - title: $gettext('Space image was uploaded successfully') + return loadingService.addTask(() => { + return clientService.owncloudSdk.files + .putFileContents(`/spaces/${selectedSpace.id}/.space/${file.name}`, file, { + headers: extraHeaders, + overwrite: true + }) + .then((image) => { + return graphClient.drives + .updateDrive( + selectedSpace.id as string, + { + special: [ + { + specialFolder: { + name: 'image' + }, + id: image['OC-FileId'] + } + ] + } as Drive, + {} + ) + .then(({ data }) => { + store.commit('runtime/spaces/UPDATE_SPACE_FIELD', { + id: selectedSpace.id as string, + field: 'spaceImageData', + value: data.special.find((special) => special.specialFolder.name === 'image') + }) + store.dispatch('showMessage', { + title: $gettext('Space image was uploaded successfully') + }) + eventBus.publish('app.files.list.load') }) - eventBus.publish('app.files.list.load') + }) + .catch((error) => { + console.error(error) + store.dispatch('showMessage', { + title: $gettext('Failed to upload space image'), + status: 'danger' }) - }) - .catch((error) => { - console.error(error) - store.dispatch('showMessage', { - title: $gettext('Failed to upload space image'), - status: 'danger' }) - }) + }) } const actions = computed(() => [ diff --git a/packages/web-app-files/src/store/actions.ts b/packages/web-app-files/src/store/actions.ts index 521b9dcf9ec..79ec627b4e6 100644 --- a/packages/web-app-files/src/store/actions.ts +++ b/packages/web-app-files/src/store/actions.ts @@ -176,7 +176,7 @@ export default { const { setProgress } = loadingCallbackArgs const promises = [] const removedFiles = [] - for (const file of files) { + for (const [i, file] of files.entries()) { const promise = clientService.webdav .deleteFile(space, file) .then(() => { @@ -208,7 +208,7 @@ export default { ) }) .finally(() => { - setProgress({ total: files.length, current: removedFiles.length }) + setProgress({ total: files.length, current: i + 1 }) }) promises.push(promise) } diff --git a/packages/web-app-files/tests/unit/composables/actions/files/useFileActionsRestore.spec.ts b/packages/web-app-files/tests/unit/composables/actions/files/useFileActionsRestore.spec.ts index e01d8cbc5b0..f6fc1e24e1e 100644 --- a/packages/web-app-files/tests/unit/composables/actions/files/useFileActionsRestore.spec.ts +++ b/packages/web-app-files/tests/unit/composables/actions/files/useFileActionsRestore.spec.ts @@ -12,6 +12,7 @@ import { useStore } from 'web-pkg/src/composables' import { unref } from 'vue' import { Resource } from 'web-client' import { FileResource, ProjectSpaceResource, SpaceResource } from 'web-client/src/helpers' +import { LoadingTaskCallbackArguments } from 'web-pkg' describe('restore', () => { afterEach(() => jest.clearAllMocks()) @@ -80,7 +81,12 @@ describe('restore', () => { it('should show message on success', () => { const { wrapper } = getWrapper({ setup: async ({ restoreResources }, { space, storeOptions }) => { - await restoreResources(space, [{ id: '1', path: '/1' }], []) + await restoreResources( + space, + [{ id: '1', path: '/1' }], + [], + mock() + ) expect(storeOptions.actions.showMessage).toHaveBeenCalledTimes(1) expect(storeOptions.modules.Files.actions.removeFilesFromTrashbin).toHaveBeenCalledTimes(