Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ParticipantSelectable): add component for participant search results and bulk selection #12850

Merged
merged 2 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
</template>

<script>
import { provide } from 'vue'

import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
import Delete from 'vue-material-design-icons/Delete.vue'
import DotsCircle from 'vue-material-design-icons/DotsCircle.vue'
Expand Down Expand Up @@ -144,6 +146,9 @@ export default {
emits: ['back', 'close'],

setup() {
// Add a visual bulk selection state for SelectableParticipant component
provide('bulkParticipantsSelection', true)

return {
breakoutRoomsStore: useBreakoutRoomsStore(),
}
Expand Down
90 changes: 78 additions & 12 deletions src/components/BreakoutRoomsEditor/SelectableParticipant.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,44 @@
-->

<template>
<label class="selectable-participant">
<label class="selectable-participant" :data-nav-id="participantNavigationId">
<input v-model="modelProxy"
:value="participant.attendeeId"
:value="value"
:aria-label="participantAriaLabel"
type="checkbox"
class="selectable-participant__checkbox">
class="selectable-participant__checkbox"
@keydown.enter="handleEnter">
<!-- Participant's avatar -->
<AvatarWrapper :id="participant.actorId"
:name="participant.displayName"
:source="participant.source || participant.actorType"
<AvatarWrapper :id="actorId"
token="new"
:name="computedName"
:source="actorType"
disable-menu
disable-tooltip
:preloaded-user-status="preloadedUserStatus"
show-user-status />
:show-user-status="showUserStatus" />

<span class="selectable-participant__content">
<span class="selectable-participant__content-name">
{{ participant.displayName }}
{{ computedName }}
</span>
<span v-if="participantStatus"
class="selectable-participant__content-subname">
{{ participantStatus }}
</span>
</span>

<IconCheck class="selectable-participant__check-icon" :size="20" />
<IconCheck v-if="isBulkSelection" class="selectable-participant__check-icon" :size="20" />
</label>
</template>

<script>
import { inject } from 'vue'

import IconCheck from 'vue-material-design-icons/Check.vue'

import { t } from '@nextcloud/l10n'

import AvatarWrapper from '../AvatarWrapper/AvatarWrapper.vue'

import { getPreloadedUserStatus, getStatusMessage } from '../../utils/userStatus.ts'
Expand All @@ -60,28 +67,81 @@ export default {
type: Array,
required: true,
},

showUserStatus: {
type: Boolean,
default: true,
},
},

emits: ['update:checked'],
emits: ['update:checked', 'click-participant'],

setup() {
// Toggles the bulk selection state of this component
const isBulkSelection = inject('bulkParticipantsSelection', false)

return {
isBulkSelection,
}
},

computed: {
modelProxy: {
get() {
return this.checked
},
set(value) {
this.$emit('update:checked', value)
this.isBulkSelection
? this.$emit('update:checked', value)
: this.$emit('click-participant', this.participant)
},
},

value() {
return this.participant.attendeeId || this.participant
},

actorId() {
return this.participant.actorId || this.participant.id
},

actorType() {
return this.participant.actorType || this.participant.source
},

computedName() {
return this.participant.displayName || this.participant.label
},

preloadedUserStatus() {
return getPreloadedUserStatus(this.participant)
},

participantStatus() {
return getStatusMessage(this.participant)
return this.participant.shareWithDisplayNameUnique
?? getStatusMessage(this.participant)
},

participantAriaLabel() {
return t('spreed', 'Add participant "{user}"', { user: this.computedName })
},

participantNavigationId() {
if (this.participant.actorType && this.participant.actorId) {
return this.participant.actorType + '_' + this.participant.actorId
} else {
return this.participant.source + '_' + this.participant.id
}
},
},

methods: {
t,

handleEnter(event) {
event.target.checked = !event.target.checked
},
}
}
</script>

Expand All @@ -97,6 +157,10 @@ export default {
border-radius: var(--border-radius-element, 32px);
line-height: 20px;

&, & * {
cursor: pointer;
}

&:hover,
&:focus-within,
&:has(:active),
Expand Down Expand Up @@ -129,6 +193,7 @@ export default {
top: 0;
left: 0;
z-index: -1;
opacity: 0;
}

&__content {
Expand All @@ -149,6 +214,7 @@ export default {
display: none;
margin-left: auto;
width: var(--default-clickable-area);
flex-shrink: 0;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
:value="searchText"
:participant-phone-item.sync="participantPhoneItem"
@select="addParticipantPhone" />
<ParticipantSearchResults :search-results="searchResults"
<ParticipantsSearchResults :search-results="searchResults"
:contacts-loading="contactsLoading"
:no-results="noResults"
scrollable
Expand All @@ -69,7 +69,7 @@ import { t } from '@nextcloud/l10n'

import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'

import ParticipantSearchResults from '../RightSidebar/Participants/ParticipantsSearchResults.vue'
import ParticipantsSearchResults from '../RightSidebar/Participants/ParticipantsSearchResults.vue'
import SelectPhoneNumber from '../SelectPhoneNumber.vue'
import ContactSelectionBubble from '../UIShared/ContactSelectionBubble.vue'
import DialpadPanel from '../UIShared/DialpadPanel.vue'
Expand All @@ -86,7 +86,7 @@ export default {
ContactSelectionBubble,
DialpadPanel,
NcTextField,
ParticipantSearchResults,
ParticipantsSearchResults,
SelectPhoneNumber,
TransitionWrapper,
// Icons
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export default {
const selectedParticipants = ref([])
provide('selectedParticipants', selectedParticipants)

// Add a visual bulk selection state for Participant component
// Add a visual bulk selection state for SelectableParticipant component
provide('bulkParticipantsSelection', true)

const dialogHeaderPrepId = `new-conversation-prepare-${useId()}`
Expand Down
58 changes: 0 additions & 58 deletions src/components/RightSidebar/Participants/Participant.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,19 +162,6 @@ describe('Participant.vue', () => {

expect(avatarEl.props('offline')).toBe(true)
})

test('renders avatar from search result', () => {
participant.label = 'Name from label'
participant.source = 'source-from-search'
participant.id = 'id-from-search'
const wrapper = mountParticipant(participant, true)
const avatarEl = wrapper.findComponent(AvatarWrapper)
expect(avatarEl.exists()).toBe(true)

expect(avatarEl.props('id')).toBe('id-from-search')
expect(avatarEl.props('name')).toBe('Name from label')
expect(avatarEl.props('source')).toBe('source-from-search')
})
})

describe('user name', () => {
Expand Down Expand Up @@ -318,14 +305,6 @@ describe('Participant.vue', () => {
participant.inCall = PARTICIPANT.CALL_FLAG.WITH_VIDEO | PARTICIPANT.CALL_FLAG.WITH_PHONE
checkStateIconsRendered(participant, VideoIcon)
})
test('does not render hand raised icon when searched', () => {
participant.inCall = PARTICIPANT.CALL_FLAG.WITH_VIDEO
participant.label = 'searched result'
getParticipantRaisedHandMock = jest.fn().mockReturnValue({ state: true })

checkStateIconsRendered(participant, null)
expect(getParticipantRaisedHandMock).not.toHaveBeenCalled()
})
})

describe('actions', () => {
Expand Down Expand Up @@ -786,41 +765,4 @@ describe('Participant.vue', () => {
})
})
})

describe('as search result', () => {
beforeEach(() => {
participant.label = 'Alice Search'
participant.source = 'users'
})

test('does not show actions for search results', () => {
const wrapper = mountParticipant(participant)

// no actions
expect(wrapper.findAllComponents(NcActionButton).exists()).toBe(false)
})

test('triggers event when clicking', async () => {
const eventHandler = jest.fn()
const wrapper = mountParticipant(participant)
wrapper.vm.$on('click-participant', eventHandler)

wrapper.find('a').trigger('click')

expect(eventHandler).toHaveBeenCalledWith(participant)
})

test('does not trigger click event when not a search result', async () => {
const eventHandler = jest.fn()
delete participant.label
delete participant.source
const wrapper = mountParticipant(participant)
wrapper.vm.$on('click-participant', eventHandler)

wrapper.find('a').trigger('click')

expect(eventHandler).not.toHaveBeenCalledWith(participant)
})
})

})
Loading
Loading