Skip to content

Commit

Permalink
Merge pull request #11351 from owncloud/feat/external-role-filter
Browse files Browse the repository at this point in the history
feat: external collaborator search filter
  • Loading branch information
JammingBen authored Aug 15, 2024
2 parents 40abad4 + dc6f910 commit 4c3dc5e
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,45 @@
<!-- Empty to hide the caret -->
<span />
</template>
<template #spinner="{ loading }">
<oc-spinner
v-if="loading"
:aria-label="$gettext('Loading users and groups')"
size="small"
/>
<oc-filter-chip
v-if="shareRoleTypes.length > 1"
:filter-label="currentShareRoleType.label"
class="invite-form-share-role-type"
raw
close-on-click
>
<template #default>
<oc-button
v-for="(option, index) in shareRoleTypes"
:key="index"
appearance="raw"
size="medium"
justify-content="space-between"
class="invite-form-share-role-type-item oc-flex oc-flex-middle oc-width-1-1 oc-py-xs oc-px-s"
@click="selectShareRoleType(option)"
>
<span>{{ option.longLabel }}</span>
<div v-if="option.id === currentShareRoleType.id" class="oc-flex">
<oc-icon name="check" />
</div>
</oc-button>
</template>
</oc-filter-chip>
</template>
</oc-select>
</div>
<div class="oc-flex oc-flex-between oc-flex-wrap oc-mb-l oc-mt-s">
<role-dropdown
mode="create"
:show-icon="isRunningOnEos"
class="role-selection-dropdown"
:is-external="isExternalShareRoleType"
@option-change="collaboratorRoleChanged"
/>
<div class="oc-flex">
Expand Down Expand Up @@ -152,14 +184,15 @@ import {
useUserStore
} from '@ownclouders/web-pkg'
import { computed, defineComponent, inject, ref, unref, watch, onMounted, nextTick } from 'vue'
import { computed, defineComponent, inject, ref, unref, watch, onMounted, nextTick, Ref } from 'vue'
import { Resource, SpaceResource } from '@ownclouders/web-client'
import { formatDateFromDateTime, formatRelativeDateFromDateTime } from '@ownclouders/web-pkg'
import { DateTime } from 'luxon'
import { OcDrop } from 'design-system/src/components'
import { useTask } from 'vue-concurrency'
import { useGettext } from 'vue3-gettext'
import { isProjectSpaceResource } from '@ownclouders/web-client'
import { Group } from '@ownclouders/web-client/graph/generated'
// just a dummy function to trick gettext tools
const $gettext = (str: string) => {
Expand All @@ -173,6 +206,8 @@ type AccountType = {
type DropDownShouldOpenOptions = { open: boolean; search: string[] }
export type ShareRoleType = { id: string; label: string; longLabel: string }
export default defineComponent({
name: 'InviteCollaboratorForm',
components: {
Expand Down Expand Up @@ -223,6 +258,7 @@ export default defineComponent({
const resource = inject<Resource>('resource')
const space = inject<SpaceResource>('space')
const availableExternalRoles = inject<Ref<ShareRole[]>>('availableExternalShareRoles')
const resourceIsSpace = computed(() => unref(resource).type === 'space')
Expand Down Expand Up @@ -288,14 +324,27 @@ export default defineComponent({
})
const fetchRecipientsTask = useTask(function* (signal, query: string) {
let filter: string
if (unref(isExternalShareRoleType)) {
// filter for external user types only
filter = `(userType eq 'Federated')`
}
const client = clientService.graphAuthenticated
const userData = yield* call(
client.users.listUsers({ orderBy: ['displayName'], search: `"${query}"` }, { signal })
client.users.listUsers(
{ orderBy: ['displayName'], search: `"${query}"`, filter },
{ signal }
)
)
const groupData = yield* call(
client.groups.listGroups({ orderBy: ['displayName'], search: `"${query}"` }, { signal })
)
let groupData: Group[]
if (!unref(isExternalShareRoleType)) {
// groups are only available for internal shares
groupData = yield* call(
client.groups.listGroups({ orderBy: ['displayName'], search: `"${query}"` }, { signal })
)
}
const users = (userData || []).map((u) => ({
...u,
Expand Down Expand Up @@ -406,6 +455,48 @@ export default defineComponent({
saving.value = false
}
const externalShareRolesEnabled = computed(() => unref(availableExternalRoles).length)
const internalShareRoleType = '1'
const externalShareRoleType = '2'
const shareRoleTypes = computed<ShareRoleType[]>(() => [
{
id: internalShareRoleType,
label: $gettext('Internal'),
longLabel: $gettext('Internal users')
},
...((unref(externalShareRolesEnabled) && [
{
id: externalShareRoleType,
label: $gettext('External'),
longLabel: $gettext('External users')
}
]) ||
[])
])
const currentShareRoleType = ref<ShareRoleType>(unref(shareRoleTypes)[0])
const isExternalShareRoleType = computed(
() => unref(currentShareRoleType).id === externalShareRoleType
)
const selectShareRoleType = async (shareRoleType: ShareRoleType) => {
if (unref(currentShareRoleType).id !== shareRoleType.id) {
currentShareRoleType.value = shareRoleType
selectedCollaborators.value = []
if (unref(searchQuery)) {
await fetchRecipients(unref(searchQuery))
}
}
focusShareInput()
}
const focusShareInput = () => {
const inviteInput = document.getElementById('files-share-invite-input')
if (inviteInput) {
inviteInput.focus()
}
}
return {
minSearchLength: capabilityRefs.sharingSearchMinLength,
isRunningOnEos: computed(() => configStore.options.runningOnEos),
Expand All @@ -423,6 +514,11 @@ export default defineComponent({
selectedCollaborators,
fetchRecipients,
share,
shareRoleTypes,
currentShareRoleType,
isExternalShareRoleType,
selectShareRoleType,
focusShareInput,
// CERN
accountType,
Expand Down Expand Up @@ -521,10 +617,9 @@ export default defineComponent({
resetFocusOnInvite(event: CollaboratorAutoCompleteItem[]) {
this.selectedCollaborators = event
this.autocompleteResults = []
this.searchQuery = ''
this.$nextTick(() => {
const inviteInput = document.getElementById('files-share-invite-input')
inviteInput.focus()
this.focusShareInput()
})
}
}
Expand All @@ -551,4 +646,26 @@ export default defineComponent({
.new-collaborators-form-cern > .cern-account-type-input {
width: 30%;
}
#new-collaborators-form {
.invite-form-share-role-type {
.oc-filter-chip-button.oc-pill {
padding: 0 !important;
}
&-item:hover {
background-color: var(--oc-color-background-hover) !important;
}
.oc-drop {
width: 180px;
}
}
.vs__actions {
padding: 0 !important;
cursor: inherit;
flex-wrap: nowrap;
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ export default defineComponent({
isLocked: {
type: Boolean,
default: false
},
// only show external share roles
isExternal: {
type: Boolean,
default: false
}
},
emits: ['optionChange'],
Expand All @@ -128,7 +133,14 @@ export default defineComponent({
return ''
})
const availableRoles = inject<Ref<ShareRole[]>>('availableShareRoles')
const availableInternalRoles = inject<Ref<ShareRole[]>>('availableInternalShareRoles')
const availableExternalRoles = inject<Ref<ShareRole[]>>('availableExternalShareRoles')
const availableRoles = computed(() => {
if (props.isExternal) {
return unref(availableExternalRoles)
}
return unref(availableInternalRoles)
})
const initialSelectedRole = props.existingRole ? props.existingRole : unref(availableRoles)[0]
const selectedRole = ref<ShareRole>(initialSelectedRole)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
} from '@ownclouders/web-client'
import { Group, User } from '@ownclouders/web-client/graph/generated'
import OcButton from 'design-system/src/components/OcButton/OcButton.vue'
import RoleDropdown from '../../../../../../../src/components/SideBar/Shares/Collaborators/RoleDropdown.vue'
import { ShareRoleType } from '../../../../../../../src/components/SideBar/Shares/Collaborators/InviteCollaborator/InviteCollaboratorForm.vue'

vi.mock('lodash-es', () => ({ debounce: (fn: any) => fn() }))

Expand Down Expand Up @@ -147,6 +149,27 @@ describe('InviteCollaboratorForm', () => {
})
it.todo('resets focus upon selecting an invitee')
})
describe('share role type filter', () => {
it.each([
{ externalRoles: [], available: false },
{ externalRoles: [mock<ShareRole>()], available: true }
])(
'is present depending on the available external share roles',
({ externalRoles, available }) => {
const { wrapper } = getWrapper({ externalShareRoles: externalRoles })
expect(wrapper.find('.invite-form-share-role-type').exists()).toBe(available)
}
)
it('correctly passes the external prop to the role dropdown component', async () => {
const externalRoles = [mock<ShareRole>()]
const { wrapper } = getWrapper({ externalShareRoles: externalRoles })
wrapper.vm.currentShareRoleType = mock<ShareRoleType>({ id: '2' })
await wrapper.vm.$nextTick()

const roleDropdown = wrapper.findComponent<typeof RoleDropdown>('role-dropdown-stub')
expect(roleDropdown.props('isExternal')).toBeTruthy()
})
})
})

function getWrapper({
Expand All @@ -155,13 +178,15 @@ function getWrapper({
users = [],
groups = [],
existingCollaborators = [],
externalShareRoles = [],
user = mock<User>({ id: '1' })
}: {
storageId?: string
resource?: Resource
users?: User[]
groups?: Group[]
existingCollaborators?: CollaboratorShare[]
externalShareRoles?: ShareRole[]
user?: User
} = {}) {
const mocks = defaultComponentMocks({
Expand Down Expand Up @@ -193,8 +218,9 @@ function getWrapper({
}
})
],
provide: { ...mocks, resource },
mocks
provide: { ...mocks, resource, availableExternalShareRoles: externalShareRoles },
mocks,
stubs: { OcSelect: false, VueSelect: false }
}
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('RoleDropdown', () => {
it('does not render a button if only one role is available', () => {
const { wrapper } = getWrapper({
mountType: shallowMount,
availableShareRoles: [mock<ShareRole>({ displayName: 'Can view', description: '' })]
availableInternalShareRoles: [mock<ShareRole>({ displayName: 'Can view', description: '' })]
})
expect(wrapper.find(selectors.recipientRoleBtn).exists()).toBeFalsy()
})
Expand All @@ -41,24 +41,42 @@ describe('RoleDropdown', () => {
const { wrapper } = getWrapper({ mountType: shallowMount })
expect(wrapper.findAll(selectors.roleButton).length).toBe(2)
})
it('uses available external share roles if "isExternal" is given', () => {
const externalShareRole2 = mock<ShareRole>({ id: 'external1', displayName: '' })
const externalShareRole1 = mock<ShareRole>({ id: 'external2', displayName: '' })
const { wrapper } = getWrapper({
mountType: shallowMount,
isExternal: true,
availableExternalShareRoles: [externalShareRole1, externalShareRole2]
})

expect(
wrapper.find(`oc-button-stub#files-recipient-role-drop-btn-${externalShareRole1.id}`).exists()
).toBeTruthy()
})
})

function getWrapper({
mountType = mount,
existingRole = null,
availableShareRoles = [
isExternal = false,
availableInternalShareRoles = [
mock<ShareRole>({ displayName: 'Can view', description: '' }),
mock<ShareRole>({ displayName: 'Can edit', description: '' })
]
],
availableExternalShareRoles = []
}: {
mountType?: typeof mount
existingRole?: ShareRole
availableShareRoles?: ShareRole[]
isExternal?: boolean
availableInternalShareRoles?: ShareRole[]
availableExternalShareRoles?: ShareRole[]
} = {}) {
return {
wrapper: mountType(RoleDropdown, {
props: {
existingRole
existingRole,
isExternal
},
global: {
plugins: [
Expand All @@ -69,7 +87,8 @@ function getWrapper({
renderStubDefaultSlot: true,
provide: {
resource: mock<Resource>(),
availableShareRoles
availableInternalShareRoles,
availableExternalShareRoles
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ exports[`Collaborator ListItem component > share inheritance indicators > show w
<div data-v-ccf14be2="" class="oc-text-truncate"><span data-v-ccf14be2="" aria-hidden="true" class="files-collaborators-collaborator-name">einstein</span> <span data-v-ccf14be2="" class="oc-invisible-sr">Share receiver name: einstein</span></div>
<div data-v-ccf14be2="">
<div data-v-ccf14be2="" class="oc-flex oc-flex-nowrap oc-flex-middle">
<role-dropdown-stub data-v-ccf14be2="" existingrole="undefined" domselector="1" mode="edit" showicon="false" islocked="false" class="files-collaborators-collaborator-role"></role-dropdown-stub>
<role-dropdown-stub data-v-ccf14be2="" existingrole="undefined" domselector="1" mode="edit" showicon="false" islocked="false" isexternal="false" class="files-collaborators-collaborator-role"></role-dropdown-stub>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/web-app-ocm/src/views/IncomingInvitations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export default defineComponent({
const { token: currentToken, providerDomain, ...query } = unref(route).query
router.replace({
name: 'ocm-app-invitations',
name: 'open-cloud-mesh-invitations',
query
})
Expand Down
Loading

0 comments on commit 4c3dc5e

Please sign in to comment.