diff --git a/changelog/unreleased/enhancement-admin-change-spaces-quota-batch-action b/changelog/unreleased/enhancement-admin-change-spaces-quota-batch-action new file mode 100644 index 00000000000..0ae3da11125 --- /dev/null +++ b/changelog/unreleased/enhancement-admin-change-spaces-quota-batch-action @@ -0,0 +1,6 @@ +Enhancement: Batch edit quota in admin panel + +We've added the batch edit quota functionality to the admin panel for users personal space and in the spaces list + +https://github.com/owncloud/web/pull/8387 +https://github.com/owncloud/web/issues/8417 \ No newline at end of file diff --git a/packages/web-app-admin-settings/src/components/General/AppearanceSection.vue b/packages/web-app-admin-settings/src/components/General/AppearanceSection.vue index 9d596e1f91f..123030fce6c 100644 --- a/packages/web-app-admin-settings/src/components/General/AppearanceSection.vue +++ b/packages/web-app-admin-settings/src/components/General/AppearanceSection.vue @@ -51,10 +51,10 @@ import { useStore } from 'web-pkg' export default defineComponent({ name: 'AppearanceSection', - mixins: [UploadLogo, ResetLogo], components: { ContextActionMenu }, + mixins: [UploadLogo, ResetLogo], setup() { const store = useStore() const instance = getCurrentInstance().proxy as any diff --git a/packages/web-app-admin-settings/src/components/Spaces/ContextActions.vue b/packages/web-app-admin-settings/src/components/Spaces/ContextActions.vue index 80609fea8db..46a41919187 100644 --- a/packages/web-app-admin-settings/src/components/Spaces/ContextActions.vue +++ b/packages/web-app-admin-settings/src/components/Spaces/ContextActions.vue @@ -4,7 +4,7 @@ @@ -77,8 +77,6 @@ export default defineComponent({ } return sections }) - - const quotaModalSelectedSpace = computed(() => instance.$data.$_editQuota_selectedSpace) const quotaModalIsOpen = computed(() => instance.$data.$_editQuota_modalOpen) const closeQuotaModal = () => { instance.$_editQuota_closeModal() @@ -88,7 +86,6 @@ export default defineComponent({ } return { menuSections, - quotaModalSelectedSpace, quotaModalIsOpen, closeQuotaModal, spaceQuotaUpdated diff --git a/packages/web-app-admin-settings/src/components/Spaces/SideBar/ActionsPanel.vue b/packages/web-app-admin-settings/src/components/Spaces/SideBar/ActionsPanel.vue index 0f35b1a9c47..231af29a132 100644 --- a/packages/web-app-admin-settings/src/components/Spaces/SideBar/ActionsPanel.vue +++ b/packages/web-app-admin-settings/src/components/Spaces/SideBar/ActionsPanel.vue @@ -3,7 +3,7 @@ diff --git a/packages/web-app-admin-settings/src/components/Users/ContextActions.vue b/packages/web-app-admin-settings/src/components/Users/ContextActions.vue index 1ac640f42e6..799e6184117 100644 --- a/packages/web-app-admin-settings/src/components/Users/ContextActions.vue +++ b/packages/web-app-admin-settings/src/components/Users/ContextActions.vue @@ -1,6 +1,12 @@ @@ -8,14 +14,27 @@ import ShowDetails from '../../mixins/showDetails' import Delete from '../../mixins/users/delete' import Edit from '../../mixins/users/edit' -import { computed, defineComponent, getCurrentInstance, PropType, unref } from 'vue' +import { + computed, + defineComponent, + getCurrentInstance, + PropType, + unref, + watch, + toRaw, + ref +} from 'vue' import ContextActionMenu from 'web-pkg/src/components/ContextActions/ContextActionMenu.vue' import { User } from 'web-client/src/generated' +import QuotaModal from 'web-pkg/src/components/Spaces/QuotaModal.vue' +import EditQuota from 'web-pkg/src/mixins/spaces/editQuota' +import { SpaceResource } from 'web-client/src' +import { useGettext } from 'vue3-gettext' export default defineComponent({ name: 'ContextActions', - components: { ContextActionMenu }, - mixins: [Delete, Edit, ShowDetails], + components: { ContextActionMenu, QuotaModal }, + mixins: [Delete, Edit, ShowDetails, EditQuota], props: { items: { type: Array as PropType, @@ -24,13 +43,36 @@ export default defineComponent({ }, setup(props) { const instance = getCurrentInstance().proxy as any - + const { $gettext } = useGettext() const filterParams = computed(() => ({ resources: props.items })) + const selectedPersonalDrives = ref([]) + watch( + () => props.items, + async () => { + selectedPersonalDrives.value.splice(0, unref(selectedPersonalDrives).length) + props.items.forEach((user) => { + const drive = toRaw(user.drive) + if (drive === undefined || drive.id === undefined) { + return + } + const spaceResource = { + id: drive.id, + name: $gettext(' of %{name}', { name: user.displayName }), + spaceQuota: drive.quota + } as SpaceResource + selectedPersonalDrives.value.push(spaceResource) + }) + }, + { deep: true, immediate: true } + ) const menuItemsPrimaryActions = computed(() => [...instance.$_edit_items, ...instance.$_delete_items].filter((item) => item.isEnabled(unref(filterParams)) ) ) + const menuItemsSecondaryActions = computed(() => + [...instance.$_editQuota_items].filter((item) => item.isEnabled(unref(filterParams))) + ) const menuItemsSidebar = computed(() => [...instance.$_showDetails_items].filter((item) => item.isEnabled(unref(filterParams))) @@ -45,6 +87,12 @@ export default defineComponent({ items: unref(menuItemsPrimaryActions) }) } + if (unref(menuItemsSecondaryActions).length) { + sections.push({ + name: 'secondaryActions', + items: unref(menuItemsSecondaryActions) + }) + } if (unref(menuItemsSidebar).length) { sections.push({ name: 'sidebar', @@ -54,8 +102,20 @@ export default defineComponent({ return sections }) + const quotaModalIsOpen = computed(() => instance.$data.$_editQuota_modalOpen) + const closeQuotaModal = () => { + instance.$_editQuota_closeModal() + } + const spaceQuotaUpdated = (quota) => { + instance.$data.$_editQuota_selectedSpace.spaceQuota = quota + } + return { - menuSections + menuSections, + quotaModalIsOpen, + closeQuotaModal, + spaceQuotaUpdated, + selectedPersonalDrives } } }) diff --git a/packages/web-app-admin-settings/src/views/Spaces.vue b/packages/web-app-admin-settings/src/views/Spaces.vue index 23c6ebee1a4..ab2ab14939e 100644 --- a/packages/web-app-admin-settings/src/views/Spaces.vue +++ b/packages/web-app-admin-settings/src/views/Spaces.vue @@ -38,6 +38,12 @@ @@ -95,6 +101,7 @@ import ContextActions from '../components/Users/ContextActions.vue' import DetailsPanel from '../components/Users/SideBar/DetailsPanel.vue' import EditPanel from '../components/Users/SideBar/EditPanel.vue' import BatchActions from 'web-pkg/src/components/BatchActions.vue' +import QuotaModal from 'web-pkg/src/components/Spaces/QuotaModal.vue' import Delete from '../mixins/users/delete' import { queryItemAsString, @@ -120,6 +127,10 @@ import AppTemplate from '../components/AppTemplate.vue' import { useSideBar } from 'web-pkg/src/composables/sideBar' import ItemFilter from 'web-pkg/src/components/ItemFilter.vue' import AppLoadingSpinner from 'web-pkg/src/components/AppLoadingSpinner.vue' +import EditQuota from 'web-pkg/src/mixins/spaces/editQuota' +import { toRaw } from 'vue' +import { SpaceResource } from 'web-client/src' +import { useGettext } from 'vue3-gettext' export default defineComponent({ name: 'UsersView', @@ -130,11 +141,13 @@ export default defineComponent({ CreateUserModal, BatchActions, ContextActions, - ItemFilter + ItemFilter, + QuotaModal }, - mixins: [Delete], + mixins: [Delete, EditQuota], setup() { const instance = getCurrentInstance().proxy as any + const { $gettext } = useGettext() const store = useStore() const accessToken = useAccessToken({ store }) const { graphClient } = useGraphClient() @@ -190,26 +203,45 @@ export default defineComponent({ loadUsersTask.perform(groupIds) } + const selectedPersonalDrives = ref([]) watch( - selectedUsers, + () => unref(selectedUsers).length, async () => { - const loadAdditionalData = unref(selectedUsers).length === 1 - if (loadAdditionalData && unref(loadedUser)?.id === unref(selectedUsers)[0].id) { - // current user is already loaded - return - } - sideBarLoading.value = true - if (loadAdditionalData) { - loadedUser.value = await loadAdditionalUserDataTask.perform(unref(selectedUsers)[0]) - sideBarLoading.value = false - return - } + // Load additional user data + const requests = [] + unref(selectedUsers).forEach((user) => { + requests.push(loadAdditionalUserDataTask.perform(user)) + }) - loadedUser.value = null + const loadedUsers = await Promise.all(requests) + unref(selectedUsers).forEach((user) => { + const additionalUserData = loadedUsers.find((loadedUser) => loadedUser.id === user.id) + Object.assign(user, additionalUserData) + if (unref(selectedUsers).length === 1) { + loadedUser.value = additionalUserData + sideBarLoading.value = false + return + } + }) + if (unref(selectedUsers).length !== 1) { + loadedUser.value = null + } sideBarLoading.value = false - }, - { deep: true } + selectedPersonalDrives.value.splice(0, unref(selectedPersonalDrives).length) + unref(selectedUsers).forEach((user) => { + const drive = toRaw(user.drive) + if (drive === undefined || drive.id === undefined) { + return + } + const spaceResource = { + id: drive.id, + name: $gettext(' of %{name}', { name: user.displayName }), + spaceQuota: drive.quota + } as SpaceResource + selectedPersonalDrives.value.push(spaceResource) + }) + } ) const calculateListHeaderPosition = () => { @@ -217,7 +249,7 @@ export default defineComponent({ } const batchActions = computed(() => { - return [...instance.$_delete_items].filter((item) => + return [...instance.$_delete_items, ...instance.$_editQuota_items].filter((item) => item.isEnabled({ resources: unref(selectedUsers) }) ) }) @@ -245,6 +277,14 @@ export default defineComponent({ eventBus.unsubscribe('app.admin-settings.users.user.updated', userUpdatedEventToken) }) + const quotaModalIsOpen = computed(() => instance.$data.$_editQuota_modalOpen) + const closeQuotaModal = () => { + instance.$_editQuota_closeModal() + } + const spaceQuotaUpdated = (quota) => { + instance.$data.$_editQuota_selectedSpace.spaceQuota = quota + } + return { ...useSideBar(), template, @@ -261,7 +301,11 @@ export default defineComponent({ listHeaderPosition, createUserModalOpen, batchActions, - filterGroups + filterGroups, + quotaModalIsOpen, + closeQuotaModal, + spaceQuotaUpdated, + selectedPersonalDrives } }, computed: { diff --git a/packages/web-app-admin-settings/tests/unit/views/Spaces.spec.ts b/packages/web-app-admin-settings/tests/unit/views/Spaces.spec.ts index 8797617ca89..7433d047252 100644 --- a/packages/web-app-admin-settings/tests/unit/views/Spaces.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/views/Spaces.spec.ts @@ -27,7 +27,7 @@ const selectors = { batchActionsStub: 'batch-actions-stub' } -const mixins = ['$_disable_items', '$_restore_items', '$_delete_items'] +const mixins = ['$_disable_items', '$_restore_items', '$_delete_items', '$_editQuota_items'] jest.mock('web-pkg/src/composables/appDefaults') describe('Spaces view', () => { 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 9fb04d68456..8ee773617f0 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 @@ -8,12 +8,14 @@ import { defaultComponentMocks, defaultPlugins, defaultStoreMockOptions, + getActionMixinMocks, mount, shallowMount } from 'web-test-helpers' import { AxiosResponse } from 'axios' import { queryItemAsString } from 'web-pkg' +const mixins = ['$_delete_items', '$_editQuota_items'] jest.mock('web-pkg/src/composables/appDefaults') const getDefaultUser = () => { @@ -373,7 +375,8 @@ function getMountedWrapper({ } = {}) { jest.mocked(queryItemAsString).mockImplementation(() => queryItem) const mocks = { - ...defaultComponentMocks() + ...defaultComponentMocks(), + ...getActionMixinMocks({ actions: mixins }) } mocks.$clientService.graphAuthenticated.mockImplementation(() => graph) @@ -388,19 +391,23 @@ function getMountedWrapper({ return { mocks, - wrapper: mountType(Users, { - global: { - plugins: [...defaultPlugins(), store], - mocks, - stubs: { - CreateUserModal: true, - AppLoadingSpinner: true, - OcBreadcrumb: true, - OcTable: true, - ItemFilter: true, - BatchActions: true + wrapper: mountType( + { ...Users, mixins }, + { + global: { + plugins: [...defaultPlugins(), store], + mocks, + stubs: { + CreateUserModal: true, + AppLoadingSpinner: true, + OcBreadcrumb: true, + NoContentMessage: true, + OcTable: true, + ItemFilter: true, + BatchActions: true + } } } - }) + ) } } diff --git a/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Spaces.spec.ts.snap b/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Spaces.spec.ts.snap index 9c1754e6408..813fcec5969 100644 --- a/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Spaces.spec.ts.snap +++ b/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Spaces.spec.ts.snap @@ -20,6 +20,7 @@ exports[`Spaces view loading states should render spaces list after loading has +
diff --git a/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue b/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue index 14a5fa5ea9a..f5ba5036b3f 100644 --- a/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue +++ b/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue @@ -5,7 +5,7 @@ :cancel="closeReadmeContentModal" :space="resources[0]" > - + ({ path: '/files' }) }) + ...defaultComponentMocks({ currentRoute: mock({ path: '/files' }) }), + $permissionManager: { + canEditSpaceQuota: () => true + } }, plugins: [...defaultPlugins(), store] } diff --git a/packages/web-app-files/tests/unit/components/Spaces/SpaceHeader.spec.ts b/packages/web-app-files/tests/unit/components/Spaces/SpaceHeader.spec.ts index c12b15c5111..189823ba871 100644 --- a/packages/web-app-files/tests/unit/components/Spaces/SpaceHeader.spec.ts +++ b/packages/web-app-files/tests/unit/components/Spaces/SpaceHeader.spec.ts @@ -43,7 +43,10 @@ describe('SpaceHeader', () => { function getWrapper({ space = {}, sideBarOpen = false }) { const mocks = { - ...defaultComponentMocks() + ...defaultComponentMocks(), + $permissionManager: { + canEditSpaceQuota: () => true + } } const store = createStore(defaultStoreMockOptions) return mount(SpaceHeader, { @@ -53,7 +56,10 @@ function getWrapper({ space = {}, sideBarOpen = false }) { }, global: { mocks, - plugins: [...defaultPlugins(), store] + plugins: [...defaultPlugins(), store], + stubs: { + 'quota-modal': true + } } }) } diff --git a/packages/web-app-files/tests/unit/components/Spaces/__snapshots__/SpaceContextActions.spec.ts.snap b/packages/web-app-files/tests/unit/components/Spaces/__snapshots__/SpaceContextActions.spec.ts.snap index a69115ba5df..92583e071e8 100644 --- a/packages/web-app-files/tests/unit/components/Spaces/__snapshots__/SpaceContextActions.spec.ts.snap +++ b/packages/web-app-files/tests/unit/components/Spaces/__snapshots__/SpaceContextActions.spec.ts.snap @@ -16,6 +16,19 @@ exports[`SpaceContextActions action handlers renders actions that are always ava +
    +
  • + +
  • +
+
    +
  • + +
  • +
+
    +
  • + +
  • +
+
    +
  • + +
  • +