diff --git a/packages/web-app-admin-settings/src/composables/actions/groups/useGroupActionsDelete.ts b/packages/web-app-admin-settings/src/composables/actions/groups/useGroupActionsDelete.ts index 30ef3fad438..a61489625ab 100644 --- a/packages/web-app-admin-settings/src/composables/actions/groups/useGroupActionsDelete.ts +++ b/packages/web-app-admin-settings/src/composables/actions/groups/useGroupActionsDelete.ts @@ -1,6 +1,6 @@ import { computed } from 'vue' import { Store } from 'vuex' -import { eventBus } from 'web-pkg' +import { eventBus, useLoadingService } from 'web-pkg' import { useClientService, useStore } from 'web-pkg/src/composables' import { GroupAction, GroupActionOptions } from 'web-pkg/src/composables/actions' import { useGettext } from 'vue3-gettext' @@ -9,12 +9,15 @@ export const useGroupActionsDelete = ({ store }: { store?: Store }) => { store = store || useStore() const { $gettext, $ngettext } = useGettext() const clientService = useClientService() + const loadingService = useLoadingService() const deleteGroups = async (groups) => { const graphClient = clientService.graphAuthenticated const promises = groups.map((group) => graphClient.groups.deleteGroup(group.id)) - const results = await Promise.allSettled(promises) + const results = await loadingService.addTask(() => { + return Promise.allSettled(promises) + }) const succeeded = results.filter((r) => r.status === 'fulfilled') if (succeeded.length) { @@ -45,7 +48,10 @@ export const useGroupActionsDelete = ({ store }: { store?: Store }) => { { groupCount: failed.length.toString() }, true ) - store.dispatch('showErrorMessage', { title }) + store.dispatch('showErrorMessage', { + title, + errors: (failed as PromiseRejectedResult[]).map((f) => f.reason) + }) } store.dispatch('hideModal') diff --git a/packages/web-app-admin-settings/src/composables/actions/users/useUserActionsDelete.ts b/packages/web-app-admin-settings/src/composables/actions/users/useUserActionsDelete.ts index a7adccc7284..2d0842be1cd 100644 --- a/packages/web-app-admin-settings/src/composables/actions/users/useUserActionsDelete.ts +++ b/packages/web-app-admin-settings/src/composables/actions/users/useUserActionsDelete.ts @@ -1,6 +1,6 @@ import { computed, unref } from 'vue' import { Store } from 'vuex' -import { eventBus, useCapabilityDeleteUsersDisabled } from 'web-pkg' +import { eventBus, useCapabilityDeleteUsersDisabled, useLoadingService } from 'web-pkg' import { useClientService, useStore } from 'web-pkg/src/composables' import { UserAction, UserActionOptions } from 'web-pkg/src/composables/actions' import { useGettext } from 'vue3-gettext' @@ -9,11 +9,14 @@ export const useUserActionsDelete = ({ store }: { store?: Store }) => { store = store || useStore() const { $gettext, $ngettext } = useGettext() const clientService = useClientService() + const loadingService = useLoadingService() const deleteUsers = async (users) => { const graphClient = clientService.graphAuthenticated const promises = users.map((user) => graphClient.users.deleteUser(user.id)) - const results = await Promise.allSettled(promises) + const results = await loadingService.addTask(() => { + return Promise.allSettled(promises) + }) const succeeded = results.filter((r) => r.status === 'fulfilled') if (succeeded.length) { @@ -44,7 +47,10 @@ export const useUserActionsDelete = ({ store }: { store?: Store }) => { { userCount: failed.length.toString() }, true ) - store.dispatch('showErrorMessage', { title }) + store.dispatch('showErrorMessage', { + title, + errors: (failed as PromiseRejectedResult[]).map((f) => f.reason) + }) } store.dispatch('hideModal') diff --git a/packages/web-app-admin-settings/src/views/Users.vue b/packages/web-app-admin-settings/src/views/Users.vue index 8334367b250..2fb8f290bad 100644 --- a/packages/web-app-admin-settings/src/views/Users.vue +++ b/packages/web-app-admin-settings/src/views/Users.vue @@ -363,7 +363,9 @@ export default defineComponent({ sideBarLoading.value = true // Load additional user data const requests = unref(selectedUsers).map((user) => loadAdditionalUserDataTask.perform(user)) - const results = await Promise.allSettled>(requests) + const results = await loadingService.addTask(() => { + return Promise.allSettled>(requests) + }) const failedRequests = results.filter((result) => result.status === 'rejected') if (failedRequests.length > 0) { console.debug('Failed to load additional user data', failedRequests) @@ -500,70 +502,163 @@ export default defineComponent({ } const addUsersToGroups = async ({ users: affectedUsers, groups: groupsToAdd }) => { - try { - const client = clientService.graphAuthenticated - const usersToFetch = [] - const addUsersToGroupsRequests = [] - groupsToAdd.reduce((acc, group) => { - for (const user of affectedUsers) { - if (!user.memberOf.find((userGroup) => userGroup.id === group.id)) { - acc.push(client.groups.addMember(group.id, user.id, configurationManager.serverUrl)) - if (!usersToFetch.includes(user.id)) { - usersToFetch.push(user.id) - } + const client = clientService.graphAuthenticated + const usersToFetch = [] + const promises = groupsToAdd.reduce((acc, group) => { + for (const user of affectedUsers) { + if (!user.memberOf.find((userGroup) => userGroup.id === group.id)) { + acc.push(client.groups.addMember(group.id, user.id, configurationManager.serverUrl)) + if (!usersToFetch.includes(user.id)) { + usersToFetch.push(user.id) } } - return acc - }, addUsersToGroupsRequests) - const usersResponse = await loadingService.addTask(async () => { - await Promise.all(addUsersToGroupsRequests) - return Promise.all(usersToFetch.map((userId) => client.users.getUser(userId))) + } + return acc + }, []) + if (!promises.length) { + const title = $ngettext( + 'Group assignment already added', + 'Group assignments already added', + affectedUsers.length * groupsToAdd.length + ) + store.dispatch('showMessage', { title }) + addToGroupsModalIsOpen.value = false + return + } + + const results = await loadingService.addTask(() => { + return Promise.allSettled(promises) + }) + + const succeeded = results.filter((r) => r.status === 'fulfilled') + if (succeeded.length) { + const title = + succeeded.length === 1 && groupsToAdd.length === 1 && affectedUsers.length === 1 + ? $gettext('Group assignment "%{group}" was added successfully', { + group: groupsToAdd[0].displayName + }) + : $ngettext( + '%{groupAssignmentCount} group assignment was added successfully', + '%{groupAssignmentCount} group assignments were added successfully', + succeeded.length, + { groupAssignmentCount: succeeded.length.toString() }, + true + ) + store.dispatch('showMessage', { title }) + } + + const failed = results.filter((r) => r.status === 'rejected') + if (failed.length) { + failed.forEach(console.error) + + const title = + failed.length === 1 && groupsToAdd.length === 1 && affectedUsers.length === 1 + ? $gettext('Failed to add group assignment "%{group}"', { + group: groupsToAdd[0].displayName + }) + : $ngettext( + 'Failed to add %{groupAssignmentCount} group assignment', + 'Failed to add %{groupAssignmentCount} group assignments', + failed.length, + { groupAssignmentCount: failed.length.toString() }, + true + ) + store.dispatch('showErrorMessage', { + title, + errors: (failed as PromiseRejectedResult[]).map((f) => f.reason) }) + } + + addToGroupsModalIsOpen.value = false + + try { + const usersResponse = await Promise.all( + usersToFetch.map((userId) => client.users.getUser(userId)) + ) updateLocalUsers(usersResponse.map((r) => r.data)) - await store.dispatch('showMessage', { - title: $gettext('Users were added to groups successfully') - }) - addToGroupsModalIsOpen.value = false } catch (e) { console.error(e) - await store.dispatch('showErrorMessage', { - title: $gettext('Failed add users to group'), - error: e - }) } } const removeUsersFromGroups = async ({ users: affectedUsers, groups: groupsToRemove }) => { - try { - const client = clientService.graphAuthenticated - const usersToFetch = [] - const removeUsersToGroupsRequests = [] - groupsToRemove.reduce((acc, group) => { - for (const user of affectedUsers) { - if (user.memberOf.find((userGroup) => userGroup.id === group.id)) { - acc.push(client.groups.deleteMember(group.id, user.id)) - if (!usersToFetch.includes(user.id)) { - usersToFetch.push(user.id) - } + const client = clientService.graphAuthenticated + const usersToFetch = [] + const promises = groupsToRemove.reduce((acc, group) => { + for (const user of affectedUsers) { + if (user.memberOf.find((userGroup) => userGroup.id === group.id)) { + acc.push(client.groups.deleteMember(group.id, user.id)) + if (!usersToFetch.includes(user.id)) { + usersToFetch.push(user.id) } } - return acc - }, removeUsersToGroupsRequests) - const usersResponse = await loadingService.addTask(async () => { - await Promise.all(removeUsersToGroupsRequests) - return Promise.all(usersToFetch.map((userId) => client.users.getUser(userId))) + } + return acc + }, []) + + if (!promises.length) { + const title = $ngettext( + 'Group assignment already removed', + 'Group assignments already removed', + affectedUsers.length * groupsToRemove.length + ) + store.dispatch('showMessage', { title }) + removeFromGroupsModalIsOpen.value = false + return + } + + const results = await loadingService.addTask(() => { + return Promise.allSettled(promises) + }) + + const succeeded = results.filter((r) => r.status === 'fulfilled') + if (succeeded.length) { + const title = + succeeded.length === 1 && groupsToRemove.length === 1 && affectedUsers.length === 1 + ? $gettext('Group assignment "%{group}" was deleted successfully', { + group: groupsToRemove[0].displayName + }) + : $ngettext( + '%{groupAssignmentCount} group assignment was deleted successfully', + '%{groupAssignmentCount} group assignments were deleted successfully', + succeeded.length, + { groupAssignmentCount: succeeded.length.toString() }, + true + ) + store.dispatch('showMessage', { title }) + } + + const failed = results.filter((r) => r.status === 'rejected') + if (failed.length) { + failed.forEach(console.error) + + const title = + failed.length === 1 && groupsToRemove.length === 1 && affectedUsers.length === 1 + ? $gettext('Failed to delete group assignment "%{group}"', { + group: groupsToRemove[0].displayName + }) + : $ngettext( + 'Failed to delete %{groupAssignmentCount} group assignment', + 'Failed to delete %{groupAssignmentCount} group assignments', + failed.length, + { groupAssignmentCount: failed.length.toString() }, + true + ) + store.dispatch('showErrorMessage', { + title, + errors: (failed as PromiseRejectedResult[]).map((f) => f.reason) }) + } + + removeFromGroupsModalIsOpen.value = false + + try { + const usersResponse = await Promise.all( + usersToFetch.map((userId) => client.users.getUser(userId)) + ) updateLocalUsers(usersResponse.map((r) => r.data)) - await store.dispatch('showMessage', { - title: $gettext('Users were removed from groups successfully') - }) - removeFromGroupsModalIsOpen.value = false } catch (e) { console.error(e) - await store.dispatch('showErrorMessage', { - title: $gettext('Failed remove users from group'), - error: e - }) } } @@ -574,25 +669,63 @@ export default defineComponent({ users: User[] value: boolean }) => { + affectedUsers = affectedUsers.filter(({ id }) => store.getters.user.uuid !== id) + const client = clientService.graphAuthenticated + const promises = affectedUsers.map((u) => + client.users.editUser(u.id, { accountEnabled: value }) + ) + const results = await loadingService.addTask(() => { + return Promise.allSettled(promises) + }) + + const succeeded = results.filter((r) => r.status === 'fulfilled') as any + if (succeeded.length) { + const title = + succeeded.length === 1 && affectedUsers.length === 1 + ? $gettext('Login for user "%{user}" was edited successfully', { + user: affectedUsers[0].displayName + }) + : $ngettext( + '%{userCount} user login was edited successfully', + '%{userCount} users logins edited successfully', + succeeded.length, + { userCount: succeeded.length.toString() }, + true + ) + store.dispatch('showMessage', { title }) + } + + const failed = results.filter((r) => r.status === 'rejected') + if (failed.length) { + failed.forEach(console.error) + + const title = + failed.length === 1 && affectedUsers.length === 1 + ? $gettext('Failed edit login for user "%{user}"', { + user: affectedUsers[0].displayName + }) + : $ngettext( + 'Failed to edit %{userCount} user login', + 'Failed to edit %{userCount} user logins', + failed.length, + { userCount: failed.length.toString() }, + true + ) + store.dispatch('showErrorMessage', { + title, + errors: (failed as PromiseRejectedResult[]).map((f) => f.reason) + }) + } + + editLoginModalIsOpen.value = false + try { - affectedUsers = affectedUsers.filter(({ id }) => store.getters.user.uuid !== id) - const client = clientService.graphAuthenticated - const promises = affectedUsers.map((u) => - client.users.editUser(u.id, { accountEnabled: value }) - ) const usersResponse = await loadingService.addTask(async () => { - await Promise.all(promises) - return Promise.all(affectedUsers.map((u) => client.users.getUser(u.id))) + return Promise.all(succeeded.map(({ value }) => client.users.getUser(value.data.id))) }) updateLocalUsers(usersResponse.map((r) => r.data)) - await store.dispatch('showMessage', { title: $gettext('Login was edited successfully') }) - editLoginModalIsOpen.value = false } catch (e) { console.error(e) - return store.dispatch('showErrorMessage', { - title: $gettext('Failed to edit login'), - error: e - }) } } diff --git a/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts b/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts index 92b76cab6d0..0c738143e4a 100644 --- a/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts @@ -77,13 +77,15 @@ const getClientService = () => { clientService.graphAuthenticated.users.getUser.mockResolvedValue( mock({ data: getDefaultUser() }) ) + clientService.graphAuthenticated.users.editUser.mockResolvedValue( + mock({ data: getDefaultUser() }) + ) clientService.graphAuthenticated.groups.listGroups.mockResolvedValue( mock({ data: { value: [] } }) ) clientService.graphAuthenticated.applications.listApplications.mockResolvedValue( mock({ data: { value: getDefaultApplications() } }) ) - return clientService } 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 7999205d175..545c7b33f07 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsAcceptShare.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsAcceptShare.ts @@ -74,7 +74,8 @@ export const useFileActionsAcceptShare = ({ store }: { store?: Store } = {} 'Failed to accept the selected share.', 'Failed to accept selected shares.', resources.length - ) + ), + errors }) } 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 7fd764e1f53..ae6e9e77cb8 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsDeclineShare.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsDeclineShare.ts @@ -78,7 +78,8 @@ export const useFileActionsDeclineShare = ({ store }: { store?: Store } = { 'Failed to decline the selected share', 'Failed to decline selected shares', resources.length - ) + ), + errors }) } diff --git a/packages/web-app-files/src/store/actions.ts b/packages/web-app-files/src/store/actions.ts index 2197c271c34..41a74c02b6d 100644 --- a/packages/web-app-files/src/store/actions.ts +++ b/packages/web-app-files/src/store/actions.ts @@ -204,7 +204,8 @@ export default { context.dispatch( 'showErrorMessage', { - title: title + title: title, + error }, { root: true } ) diff --git a/packages/web-pkg/src/components/Spaces/QuotaModal.vue b/packages/web-pkg/src/components/Spaces/QuotaModal.vue index 129bc68a8da..cb6753e3fdc 100644 --- a/packages/web-pkg/src/components/Spaces/QuotaModal.vue +++ b/packages/web-pkg/src/components/Spaces/QuotaModal.vue @@ -35,7 +35,7 @@ import { useGettext } from 'vue3-gettext' import QuotaSelect from 'web-pkg/src/components/QuotaSelect.vue' import { SpaceResource } from 'web-client/src' import { eventBus, useClientService, useRouter } from 'web-pkg/src' -import { useStore } from 'web-pkg/src/composables' +import { useStore, useLoadingService } from 'web-pkg/src/composables' import { Drive } from 'web-client/src/generated' export default defineComponent({ @@ -77,6 +77,7 @@ export default defineComponent({ const store = useStore() const { $gettext, $ngettext } = useGettext() const clientService = useClientService() + const loadingService = useLoadingService() const router = useRouter() const selectedOption = ref(0) @@ -181,7 +182,9 @@ export default defineComponent({ value: driveData.quota }) }) - const results = await Promise.allSettled>(requests) + const results = await loadingService.addTask(() => { + return Promise.allSettled>(requests) + }) const succeeded = results.filter((r) => r.status === 'fulfilled') if (succeeded.length) { store.dispatch('showMessage', { title: getSuccessMessage(succeeded.length) }) @@ -189,7 +192,10 @@ export default defineComponent({ const errors = results.filter((r) => r.status === 'rejected') if (errors.length) { errors.forEach(console.error) - store.dispatch('showErrorMessage', { title: getErrorMessage(errors.length) }) + store.dispatch('showErrorMessage', { + title: getErrorMessage(errors.length), + errors: (errors as PromiseRejectedResult[]).map((f) => f.reason) + }) } props.cancel() diff --git a/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsDelete.ts b/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsDelete.ts index c530b234082..55415bff5a1 100644 --- a/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsDelete.ts +++ b/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsDelete.ts @@ -1,7 +1,7 @@ import { computed, unref } from 'vue' import { useGettext } from 'vue3-gettext' import { SpaceResource } from 'web-client/src' -import { useClientService, useRoute } from 'web-pkg/src/composables' +import { useClientService, useLoadingService, useRoute } from 'web-pkg/src/composables' import { eventBus } from 'web-pkg/src/services' import { useAbility } from '../../ability' import { useStore } from '../../store' @@ -13,54 +13,70 @@ export const useSpaceActionsDelete = ({ store }: { store?: Store } = {}) => const { $gettext, $ngettext } = useGettext() const ability = useAbility() const clientService = useClientService() + const loadingService = useLoadingService() const route = useRoute() const filterResourcesToDelete = (resources: SpaceResource[]) => { return resources.filter((r) => r.canBeDeleted({ user: store.getters.user, ability })) } - const deleteSpaces = (spaces: SpaceResource[]) => { - const requests = [] - const graphClient = clientService.graphAuthenticated - spaces.forEach((space) => { - const request = graphClient.drives + const deleteSpaces = async (spaces: SpaceResource[]) => { + const client = clientService.graphAuthenticated + const promises = spaces.map((space) => + client.drives .deleteDrive(space.id.toString(), '', { headers: { Purge: 'T' } }) .then(() => { - store.dispatch('hideModal') store.commit('Files/REMOVE_FILES', [{ id: space.id }]) store.commit('runtime/spaces/REMOVE_SPACE', { id: space.id }) return true }) - .catch((error) => { - console.error(error) - store.dispatch('showErrorMessage', { - title: $gettext('Failed to delete space %{spaceName}', { - spaceName: space.name - }), - error - }) - }) - requests.push(request) + ) + const results = await loadingService.addTask(() => { + return Promise.allSettled(promises) }) - return Promise.all(requests).then((result: boolean[]) => { - if (result.filter(Boolean).length) { - store.dispatch('showMessage', { - title: $ngettext( - 'Space was deleted successfully', - 'Spaces were deleted successfully', - result.filter(Boolean).length - ) - }) - } + const succeeded = results.filter((r) => r.status === 'fulfilled') + if (succeeded.length) { + const title = + succeeded.length === 1 && spaces.length === 1 + ? $gettext('Space "%{space}" was deleted successfully', { space: spaces[0].name }) + : $ngettext( + '%{spaceCount} space was deleted successfully', + '%{spaceCount} spaces were deleted successfully', + succeeded.length, + { spaceCount: succeeded.length.toString() }, + true + ) + store.dispatch('showMessage', { title }) + } - if (unref(route).name === 'admin-settings-spaces') { - eventBus.publish('app.admin-settings.list.load') - } - }) + const failed = results.filter((r) => r.status === 'rejected') + if (failed.length) { + failed.forEach(console.error) + + const title = + failed.length === 1 && spaces.length === 1 + ? $gettext('Failed to delete space "%{space}"', { space: spaces[0].name }) + : $ngettext( + 'Failed to delete %{spaceCount} space', + 'Failed to delete %{spaceCount} spaces', + failed.length, + { spaceCount: failed.length.toString() }, + true + ) + store.dispatch('showErrorMessage', { + title, + errors: (failed as PromiseRejectedResult[]).map((f) => f.reason) + }) + } + + store.dispatch('hideModal') + if (unref(route).name === 'admin-settings-spaces') { + eventBus.publish('app.admin-settings.list.load') + } } const handler = ({ resources }: SpaceActionOptions) => { diff --git a/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsDisable.ts b/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsDisable.ts index 7195bba66ac..2dd865cbf01 100644 --- a/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsDisable.ts +++ b/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsDisable.ts @@ -5,7 +5,7 @@ import { useGettext } from 'vue3-gettext' import { useRoute, useRouter } from '../../router' import { useStore } from '../../store' import { useAbility } from '../../ability' -import { useClientService } from 'web-pkg/src/composables' +import { useClientService, useLoadingService } from 'web-pkg/src/composables' import { Store } from 'vuex' export const useSpaceActionsDisable = ({ store }: { store?: Store } = {}) => { @@ -15,57 +15,72 @@ export const useSpaceActionsDisable = ({ store }: { store?: Store } = {}) = const clientService = useClientService() const route = useRoute() const router = useRouter() + const loadingService = useLoadingService() const filterResourcesToDisable = (resources): SpaceResource[] => { return resources.filter((r) => r.canDisable({ user: store.getters.user, ability })) } - const disableSpaces = (spaces: SpaceResource[]) => { - const requests = [] - const graphClient = clientService.graphAuthenticated + const disableSpaces = async (spaces: SpaceResource[]) => { const currentRoute = unref(route) - spaces.forEach((space) => { - const request = graphClient.drives - .deleteDrive(space.id.toString()) - .then(() => { - store.dispatch('hideModal') - if (currentRoute.name === 'admin-settings-spaces') { - space.disabled = true - space.spaceQuota = { total: space.spaceQuota.total } - } - store.commit('runtime/spaces/UPDATE_SPACE_FIELD', { - id: space.id, - field: 'disabled', - value: true - }) - return true - }) - .catch((error) => { - console.error(error) - store.dispatch('showErrorMessage', { - title: $gettext('Failed to disable space %{spaceName}', { - spaceName: space.name - }), - error - }) + + const client = clientService.graphAuthenticated + const promises = spaces.map((space) => + client.drives.deleteDrive(space.id.toString()).then(() => { + if (currentRoute.name === 'files-spaces-generic') { + router.push({ name: 'files-spaces-projects' }) + } + if (currentRoute.name === 'admin-settings-spaces') { + space.disabled = true + space.spaceQuota = { total: space.spaceQuota.total } + } + store.commit('runtime/spaces/UPDATE_SPACE_FIELD', { + id: space.id, + field: 'disabled', + value: true }) - requests.push(request) + return true + }) + ) + const results = await loadingService.addTask(() => { + return Promise.allSettled(promises) }) - return Promise.all(requests).then((result: boolean[]) => { - if (result.filter(Boolean).length) { - store.dispatch('showMessage', { - title: $ngettext( - 'Space was disabled successfully', - 'Spaces were disabled successfully', - result.filter(Boolean).length - ) - }) - } + const succeeded = results.filter((r) => r.status === 'fulfilled') + if (succeeded.length) { + const title = + succeeded.length === 1 && spaces.length === 1 + ? $gettext('Space "%{space}" was disabled successfully', { space: spaces[0].name }) + : $ngettext( + '%{spaceCount} space was disabled successfully', + '%{spaceCount} spaces were disabled successfully', + succeeded.length, + { spaceCount: succeeded.length.toString() }, + true + ) + store.dispatch('showMessage', { title }) + } - if (currentRoute.name === 'files-spaces-generic') { - return router.push({ name: 'files-spaces-projects' }) - } - }) + const failed = results.filter((r) => r.status === 'rejected') + if (failed.length) { + failed.forEach(console.error) + + const title = + failed.length === 1 && spaces.length === 1 + ? $gettext('Failed to disable space "%{space}"', { space: spaces[0].name }) + : $ngettext( + 'Failed to disable %{spaceCount} space', + 'Failed to disable %{spaceCount} spaces', + failed.length, + { spaceCount: failed.length.toString() }, + true + ) + store.dispatch('showErrorMessage', { + title, + errors: (failed as PromiseRejectedResult[]).map((f) => f.reason) + }) + } + + store.dispatch('hideModal') } const handler = ({ resources }: SpaceActionOptions) => { diff --git a/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsRestore.ts b/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsRestore.ts index 47cb528c88b..4bef55058d0 100644 --- a/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsRestore.ts +++ b/packages/web-pkg/src/composables/actions/spaces/useSpaceActionsRestore.ts @@ -3,7 +3,7 @@ import { computed, unref } from 'vue' import { Store } from 'vuex' import { SpaceAction, SpaceActionOptions } from '../types' import { useRoute } from '../../router' -import { useAbility, useClientService, useStore } from 'web-pkg/src/composables' +import { useAbility, useClientService, useLoadingService, useStore } from 'web-pkg/src/composables' import { useGettext } from 'vue3-gettext' export const useSpaceActionsRestore = ({ store }: { store?: Store } = {}) => { @@ -11,17 +11,17 @@ export const useSpaceActionsRestore = ({ store }: { store?: Store } = {}) = const { $gettext, $ngettext } = useGettext() const ability = useAbility() const clientService = useClientService() + const loadingService = useLoadingService() const route = useRoute() const filterResourcesToRestore = (resources): SpaceResource[] => { return resources.filter((r) => r.canRestore({ user: store.getters.user, ability })) } - const restoreSpaces = (spaces: SpaceResource[]) => { - const requests = [] - const graphClient = clientService.graphAuthenticated - spaces.forEach((space) => { - const request = graphClient.drives + const restoreSpaces = async (spaces: SpaceResource[]) => { + const client = clientService.graphAuthenticated + const promises = spaces.map((space) => + client.drives .updateDrive( space.id.toString(), { name: space.name }, @@ -32,7 +32,6 @@ export const useSpaceActionsRestore = ({ store }: { store?: Store } = {}) = } ) .then((updatedSpace) => { - store.dispatch('hideModal') if (unref(route).name === 'admin-settings-spaces') { space.disabled = false space.spaceQuota = updatedSpace.data.quota @@ -44,28 +43,46 @@ export const useSpaceActionsRestore = ({ store }: { store?: Store } = {}) = }) return true }) - .catch((error) => { - console.error(error) - store.dispatch('showErrorMessage', { - title: $gettext('Failed to restore space %{spaceName}', { - spaceName: space.name - }), - error - }) - }) - requests.push(request) - }) - return Promise.all(requests).then((result: boolean[]) => { - if (result.filter(Boolean).length) { - store.dispatch('showMessage', { - title: $ngettext( - 'Space was enabled successfully', - 'Spaces were enabled successfully', - result.filter(Boolean).length - ) - }) - } + ) + const results = await loadingService.addTask(() => { + return Promise.allSettled(promises) }) + const succeeded = results.filter((r) => r.status === 'fulfilled') + if (succeeded.length) { + const title = + succeeded.length === 1 && spaces.length === 1 + ? $gettext('Space "%{space}" was enabled successfully', { space: spaces[0].name }) + : $ngettext( + '%{spaceCount} space was enabled successfully', + '%{spaceCount} spaces were enabled successfully', + succeeded.length, + { spaceCount: succeeded.length.toString() }, + true + ) + store.dispatch('showMessage', { title }) + } + + const failed = results.filter((r) => r.status === 'rejected') + if (failed.length) { + failed.forEach(console.error) + + const title = + failed.length === 1 && spaces.length === 1 + ? $gettext('Failed to enabled space "%{space}"', { space: spaces[0].name }) + : $ngettext( + 'Failed to enable %{spaceCount} space', + 'Failed to enable %{spaceCount} spaces', + failed.length, + { spaceCount: failed.length.toString() }, + true + ) + store.dispatch('showErrorMessage', { + title, + errors: (failed as PromiseRejectedResult[]).map((f) => f.reason) + }) + } + + store.dispatch('hideModal') } const handler = ({ resources }: SpaceActionOptions) => { diff --git a/packages/web-runtime/src/store/app.ts b/packages/web-runtime/src/store/app.ts index ec70b6966cf..16096a181d3 100644 --- a/packages/web-runtime/src/store/app.ts +++ b/packages/web-runtime/src/store/app.ts @@ -7,23 +7,24 @@ const state = { const actions = { showErrorMessage({ commit }, message) { - const getXRequestID = (error: AxiosError): string => { - if (!error) { - return - } + const getXRequestID = (error: AxiosError): string | null => { if (error.response?.headers?.['x-request-id']) { return error.response.headers['x-request-id'] } + return null } + message.status = message.status || 'danger' message.timeout = message.timeout || 0 + message.errors = message.error ? [message.error] : message.errors || [] - if (message.error) { - const xRequestID = getXRequestID(message.error) - if (xRequestID) { - message.errorLogContent = `X-Request-ID: ${xRequestID}` - } - } + const xRequestIds = message.errors + .map((error) => getXRequestID(error)) + .filter((xRequestId) => xRequestId !== null) + .map((item) => `X-Request-Id: ${item}`) + .join('\r\n') + + message.errorLogContent = xRequestIds commit('ENQUEUE_MESSAGE', message) },