Skip to content

Commit

Permalink
Merge pull request #8766 from owncloud/readonly-groups
Browse files Browse the repository at this point in the history
Add support for read-only groups
  • Loading branch information
JammingBen authored Apr 5, 2023
2 parents ef3482f + 4a955ee commit 03040fb
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Add support for read-only groups

Read-only groups are now supported. Such groups can't be edited or assigned to/removed from users. They are indicated via a lock icon in the group list and all affected inputs.

https://github.com/owncloud/web/pull/8766
https://github.com/owncloud/web/issues/8729
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@
<template #avatar="rowData">
<avatar-image :width="32" :userid="rowData.item.id" :user-name="rowData.item.displayName" />
</template>
<template #displayName="rowData">
<div class="oc-flex oc-flex-middle">
{{ rowData.item.displayName }}
<oc-icon
v-if="rowData.item.groupTypes?.includes('ReadOnly')"
v-oc-tooltip="readOnlyLabel"
name="lock"
size="small"
fill-type="line"
class="oc-ml-s"
:accessible-label="readOnlyLabel"
/>
</div>
</template>
<template #members="rowData">
{{ rowData.item.members.length }}
</template>
Expand All @@ -59,6 +73,7 @@
<oc-icon name="information" fill-type="line" />
</oc-button>
<oc-button
v-if="!item.groupTypes?.includes('ReadOnly')"
v-oc-tooltip="$gettext('Edit')"
appearance="raw"
class="oc-mr-xs quick-action-button oc-p-xs groups-table-btn-edit"
Expand All @@ -76,11 +91,6 @@
<slot name="contextMenu" :group="item" />
</template>
</context-menu-quick-action>
<!-- Editing groups is currently not supported by backend
<oc-button v-oc-tooltip="$gettext('Edit')" class="oc-ml-s" @click="$emit('clickEdit', item)">
<oc-icon size="small" name="pencil" />
</oc-button>
-->
</template>
<template #footer>
<div class="oc-text-nowrap oc-text-center oc-width-1-1 oc-my-s">
Expand All @@ -93,13 +103,14 @@
</template>

<script lang="ts">
import { defineComponent, PropType, ref, unref, ComponentPublicInstance } from 'vue'
import { defineComponent, PropType, ref, unref, ComponentPublicInstance, computed } from 'vue'
import Fuse from 'fuse.js'
import Mark from 'mark.js'
import { displayPositionedDropdown, eventBus } from 'web-pkg'
import { SideBarEventTopics } from 'web-pkg/src/composables/sideBar'
import { Group } from 'web-client/src/generated'
import ContextMenuQuickAction from 'web-pkg/src/components/ContextActions/ContextMenuQuickAction.vue'
import { useGettext } from 'vue3-gettext'
export default defineComponent({
name: 'GroupsList',
Expand All @@ -120,6 +131,7 @@ export default defineComponent({
},
emits: ['toggleSelectAllGroups', 'unSelectAllGroups', 'toggleSelectGroup'],
setup(props, { emit }) {
const { $gettext } = useGettext()
const contextMenuButtonRef = ref(undefined)
const isGroupSelected = (group) => {
Expand Down Expand Up @@ -168,14 +180,17 @@ export default defineComponent({
eventBus.publish(SideBarEventTopics.openWithPanel, 'EditPanel')
}
const readOnlyLabel = computed(() => $gettext("This group is read-only and can't be edited"))
return {
showDetails,
rowClicked,
isGroupSelected,
showContextMenuOnBtnClick,
showContextMenuOnRightClick,
contextMenuButtonRef,
showEditPanel
showEditPanel,
readOnlyLabel
}
},
data() {
Expand Down Expand Up @@ -205,6 +220,7 @@ export default defineComponent({
{
name: 'displayName',
title: this.$gettext('Group name'),
type: 'slot',
sortable: true
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div id="user-group-select-form">
<oc-select
:model-value="selectedOption"
:model-value="selectedOptions"
class="oc-mb-s"
:multiple="true"
:options="groupOptions"
Expand Down Expand Up @@ -38,8 +38,7 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, unref, watch } from 'vue'
import { computed } from 'vue'
import { computed, defineComponent, PropType, ref, unref, watch } from 'vue'
import { Group } from 'web-client/src/generated'
export default defineComponent({
Expand All @@ -56,18 +55,27 @@ export default defineComponent({
},
emits: ['selectedOptionChange'],
setup(props, { emit }) {
const selectedOption = ref(props.selectedGroups)
const selectedOptions = ref()
const onUpdate = (event) => {
selectedOption.value = event
emit('selectedOptionChange', unref(selectedOption))
selectedOptions.value = event
emit('selectedOptionChange', unref(selectedOptions))
}
const currentGroups = computed(() => props.selectedGroups)
watch(currentGroups, () => {
selectedOption.value = props.selectedGroups
})
watch(
currentGroups,
() => {
selectedOptions.value = props.selectedGroups
.map((g) => ({
...g,
readonly: g.groupTypes?.includes('ReadOnly')
}))
.sort((a: any, b: any) => b.readonly - a.readonly)
},
{ immediate: true }
)
return { selectedOption, onUpdate }
return { selectedOptions, onUpdate }
}
})
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@confirm="$emit('confirm', { users, groups: selectedOptions })"
>
<template #content>
<GroupSelect
<group-select
:selected-groups="selectedOptions"
:group-options="groups"
@selected-option-change="changeSelectedGroupOption"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ export default defineComponent({
})
const groupOptions = computed(() => {
const { memberOf: selectedGroups } = unref(editUser)
return props.groups.filter((g) => !selectedGroups.some((s) => s.id === g.id))
return props.groups.filter(
(g) => !selectedGroups.some((s) => s.id === g.id) && !g.groupTypes?.includes('ReadOnly')
)
})
const isLoginInputDisabled = computed(() => currentUser.uuid === (props.user as User).id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const useGroupActionsDelete = ({ store }: { store?: Store<any> }) => {
},
handler,
isEnabled: ({ resources }) => {
return !!resources.length
return !!resources.length && !resources.some((r) => r.groupTypes?.includes('ReadOnly'))
},
componentType: 'button',
class: 'oc-groups-actions-delete-trigger'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const useGroupActionsEdit = () => {
label: () => $gettext('Edit'),
handler: () => eventBus.publish(SideBarEventTopics.openWithPanel, 'EditPanel'),
isEnabled: ({ resources }) => {
return resources.length > 0
return resources.length === 1 && !resources[0].groupTypes?.includes('ReadOnly')
},
componentType: 'button',
class: 'oc-groups-actions-edit-trigger'
Expand Down
4 changes: 3 additions & 1 deletion packages/web-app-admin-settings/src/views/Groups.vue
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ export default defineComponent({
title: this.$gettext('Edit group'),
component: EditPanel,
default: false,
enabled: this.selectedGroups.length === 1,
enabled:
this.selectedGroups.length === 1 &&
!this.selectedGroups[0].groupTypes?.includes('ReadOnly'),
componentAttrs: {
group: this.selectedGroups.length === 1 ? this.selectedGroups[0] : null,
onConfirm: this.editGroup
Expand Down
13 changes: 9 additions & 4 deletions packages/web-app-admin-settings/src/views/Users.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,15 @@
<groups-modal
v-if="addToGroupsModalIsOpen"
:title="addToGroupsModalTitle"
:groups="groups"
:groups="writableGroups"
:users="selectedUsers"
@cancel="() => (addToGroupsModalIsOpen = false)"
@confirm="addUsersToGroups"
/>
<groups-modal
v-if="removeFromGroupsModalIsOpen"
:title="removeFromGroupsModalTitle"
:groups="groups"
:groups="writableGroups"
:users="selectedUsers"
@cancel="() => (removeFromGroupsModalIsOpen = false)"
@confirm="removeUsersFromGroups"
Expand Down Expand Up @@ -180,7 +180,7 @@ import {
useUserActionsAddToGroups
} from '../composables/actions/users'
import { configurationManager } from 'web-pkg'
import { Drive } from 'web-client/src/generated'
import { Drive, Group } from 'web-client/src/generated'
export default defineComponent({
name: 'UsersView',
Expand Down Expand Up @@ -505,6 +505,10 @@ export default defineComponent({
}
}
const writableGroups = computed<Group[]>(() => {
return unref(groups).filter((g) => !g.groupTypes?.includes('ReadOnly'))
})
return {
...useSideBar(),
maxQuota: useCapabilitySpacesMaxQuota(),
Expand Down Expand Up @@ -533,7 +537,8 @@ export default defineComponent({
spaceQuotaUpdated,
selectedPersonalDrives,
addUsersToGroups,
removeUsersFromGroups
removeUsersFromGroups,
writableGroups
}
},
computed: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,33 @@ import { defaultPlugins, shallowMount } from 'web-test-helpers'
import { mock } from 'jest-mock-extended'
import { Group } from 'web-client/src/generated'

const groupMock = mock<Group>({ id: '1' })
const groupMock = mock<Group>({ id: '1', groupTypes: [] })

describe('GroupSelect', () => {
it('renders the select input', () => {
const { wrapper } = getWrapper()
expect(wrapper.html()).toMatchSnapshot()
})
it('correctly maps the read-only state', () => {
const groupMock = mock<Group>({ id: '1', groupTypes: ['ReadOnly'] })
const { wrapper } = getWrapper(groupMock)
expect(wrapper.vm.selectedOptions[0].readonly).toBeTruthy()
})
it('emits "selectedOptionChange" on update', () => {
const group = mock<Group>({ id: '2' })
const group = mock<Group>({ id: '2', groupTypes: [] })
const { wrapper } = getWrapper()
wrapper.vm.onUpdate(group)
expect(wrapper.emitted().selectedOptionChange).toBeTruthy()
expect(wrapper.vm.selectedOption).toEqual(group)
expect(wrapper.vm.selectedOptions).toEqual(group)
})
})

function getWrapper() {
function getWrapper(group = groupMock) {
return {
wrapper: shallowMount(GroupSelect, {
props: {
selectedGroups: [groupMock],
groupOptions: [groupMock]
selectedGroups: [group],
groupOptions: [group]
},
global: {
plugins: [...defaultPlugins()]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { Group } from 'web-client/src/generated'
import { AxiosResponse } from 'axios'

const availableGroupOptions = [
mock<Group>({ id: '1', displayName: 'group1' }),
mock<Group>({ id: '2', displayName: 'group2' })
mock<Group>({ id: '1', displayName: 'group1', groupTypes: [] }),
mock<Group>({ id: '2', displayName: 'group2', groupTypes: [] })
]
const selectors = {
groupSelectStub: 'group-select-stub'
Expand Down Expand Up @@ -132,9 +132,24 @@ describe('EditPanel', () => {
expect(wrapper.vm.invalidFormData).toBeTruthy()
})
})

describe('group select', () => {
it('takes all available groups', () => {
const { wrapper } = getWrapper()
expect(wrapper.findComponent<any>('group-select-stub').props('groupOptions').length).toBe(
availableGroupOptions.length
)
})
it('filters out read-only groups', () => {
const { wrapper } = getWrapper({
groups: [mock<Group>({ id: '1', displayName: 'group1', groupTypes: ['ReadOnly'] })]
})
expect(wrapper.findComponent<any>('group-select-stub').props('groupOptions').length).toBe(0)
})
})
})

function getWrapper({ selectedGroups = [] } = {}) {
function getWrapper({ selectedGroups = [], groups = availableGroupOptions } = {}) {
const mocks = defaultComponentMocks()
const storeOptions = defaultStoreMockOptions
const store = createStore(storeOptions)
Expand All @@ -151,7 +166,7 @@ function getWrapper({ selectedGroups = [] } = {}) {
memberOf: selectedGroups
},
roles: [{ id: '1', displayName: 'admin' }],
groups: availableGroupOptions
groups
},
global: {
mocks,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

exports[`GroupSelect renders the select input 1`] = `
<div id="user-group-select-form">
<oc-select-stub class="oc-mb-s" clearable="false" disabled="false" filter="[Function]" fixmessageline="true" id="oc-select-1" label="Groups" loading="false" model-value="undefined" multiple="true" optionlabel="displayName" options="undefined" searchable="true"></oc-select-stub>
<oc-select-stub class="oc-mb-s" clearable="false" disabled="false" filter="[Function]" fixmessageline="true" id="oc-select-1" label="Groups" loading="false" model-value="[object Object]" multiple="true" optionlabel="displayName" options="undefined" searchable="true"></oc-select-stub>
</div>
`;
Loading

0 comments on commit 03040fb

Please sign in to comment.