Skip to content
Draft
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
43 changes: 0 additions & 43 deletions src/components/AppContent/CircleContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,10 @@
props: {
loading: {
type: Boolean,
default: true,

Check warning on line 57 in src/components/AppContent/CircleContent.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Boolean prop should only be defaulted to false
},
},

data() {
return {
loadingList: false,
}
},

computed: {
// store variables
circles() {
Expand All @@ -82,25 +76,10 @@
return Object.values(this.circle?.members || [])
},

/**
* Is the current circle empty
*
* @return {boolean}
*/
isEmptyCircle() {
return this.members.length === 0
},

...mapStores(useUserGroupStore),
},

watch: {
circle(newCircle) {
if (newCircle?.id) {
this.fetchCircleMembers(newCircle.id)
}
},

userGroup(newUserGroup) {
if (newUserGroup?.id) {
this.fetchUserGroupMembers(newUserGroup.id)
Expand All @@ -109,40 +88,18 @@
},

beforeMount() {
if (this.circle?.id) {
this.fetchCircleMembers(this.circle.id)
}

if (this.userGroup?.id) {
this.fetchUserGroupMembers(this.userGroup.id)
}
},

methods: {
async fetchCircleMembers(circleId) {
this.loadingList = true
this.logger.debug('Fetching members for', { circleId })

try {
await this.$store.dispatch('getCircleMembers', circleId)
} catch (error) {
console.error(error)
showError(t('contacts', 'There was an error fetching the member list'))
} finally {
this.loadingList = false
}
},

async fetchUserGroupMembers(userGroupId) {
this.loadingList = true

try {
await this.userGroupStore.getUserGroupMembers(userGroupId)
} catch (error) {
console.error(error)
showError(t('contacts', 'There was an error fetching the member list'))
} finally {
this.loadingList = false
}
},
},
Expand Down
147 changes: 131 additions & 16 deletions src/components/MemberList/MemberList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<section class="member-list">
<NcEmptyContent v-if="loading" class="empty-content" :name="t('contacts', 'Loading members list …')">
<template #icon>
<IconLoading :size="20" />
<NcLoadingIcon :size="20" />
</template>
</NcEmptyContent>

Expand All @@ -29,17 +29,35 @@
</template>
</NcEmptyContent>

<VList
v-else
v-slot="{ item }"
class="member-list__virtual"
:style="virtualListStyle"
:data="flatList">
<MemberGridItem
:key="`member-grid-item-${item.id}`"
:member="item"
:is-team="!item.isUser" />
</VList>
<template v-else>
<div style="display: flex; margin-bottom: 2rem;">
<NcTextField
v-model="searchQuery"
:label="t('contacts', 'Search among current members')"
trailing-button-icon="close"
:show-trailing-button="searchQuery !== ''"
@trailing-button-click="clearSearchField">
<IconSearch :size="20" />
</NcTextField>

<NcSelect
v-model="searchRole"
:options="roles"
:multiple="false"
style="margin-top: 6px;min-width: 200px;margin-left: 1rem;" />
</div>

<VList
v-slot="{ item }"
class="member-list__virtual"
:style="virtualListStyle"
:data="flatList">
<MemberGridItem
:key="`member-grid-item-${item.id}`"
:member="item"
:is-team="!item.isUser" />
</VList>
</template>

<!-- member picker -->
<EntityPicker
Expand All @@ -62,26 +80,38 @@
import { showError, showWarning } from '@nextcloud/dialogs'
import { subscribe } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import { NcEmptyContent } from '@nextcloud/vue'
import { NcEmptyContent, NcLoadingIcon, NcSelect, NcTextField } from '@nextcloud/vue'
import { refDebounced } from '@vueuse/core'
import { VList } from 'virtua/vue'
import { defineComponent } from 'vue'
import { defineComponent, ref } from 'vue'
import IconContact from 'vue-material-design-icons/AccountMultipleOutline.vue'
import IconSearch from 'vue-material-design-icons/Magnify.vue'
import EntityPicker from '../EntityPicker/EntityPicker.vue'
import MemberGridItem from './MemberGridItem.vue'
import IsMobileMixin from '../../mixins/IsMobileMixin.ts'
import RouterMixin from '../../mixins/RouterMixin.js'
import { CIRCLES_MEMBER_GROUPING, SHARES_TYPES_MEMBER_MAP } from '../../models/constants.ts'
import {
CIRCLES_MEMBER_GROUPING,
CIRCLES_MEMBER_LEVELS,
MAX_MEMBERS_TO_RENDER,
MemberLevels,
SHARES_TYPES_MEMBER_MAP,
} from '../../models/constants.ts'
import { getRecommendations, getSuggestions } from '../../services/collaborationAutocompletion.js'

export default defineComponent({
name: 'MemberList',

components: {
IconSearch,
NcTextField,
NcSelect,
EntityPicker,
IconContact,
MemberGridItem,
NcEmptyContent,
VList,
NcLoadingIcon,
},

mixins: [IsMobileMixin, RouterMixin],
Expand All @@ -98,8 +128,35 @@ export default defineComponent({
},
},

setup() {
const searchQuery = ref('')
const clearSearchField = () => {
searchQuery.value = ''
}
const searchQueryDebounced = refDebounced(searchQuery, 500)

const searchRole = ref(null)
const roles = Object.entries(CIRCLES_MEMBER_LEVELS).map(([id, label]) => ({
id: Number(id),
label,
}))
roles.unshift({
id: Number(MemberLevels.NONE),
label: t('contacts', 'Pending'),
})

return {
searchQuery,
searchQueryDebounced,
clearSearchField,
searchRole,
roles,
}
},

data() {
return {
loadingList: false,
pickerLoading: false,
showPicker: false,
showPickerIntro: true,
Expand All @@ -118,12 +175,30 @@ export default defineComponent({
/**
* Return the current circle
*
* @return {object}
* @return {Circle}
*/
circle() {
return this.$store.getters.getCircle(this.selectedCircle)
},

members() {
return Object.values(this.$store.getters.getCircle(this.circle.id)?.members || [])
},

membershipTooLargeMessage() {
if (this.searchQueryDebounced || this.searchRole?.id) {
const searchQuery = this.searchQueryDebounced || '-'
const searchRole = this.searchRole?.label || 'any'
return `Search results (query: ${searchQuery}, role: ${searchRole}) contains too many entries.`
}

return 'Users list too large'
},

isMembersLisTooLarge() {
return this.flatList.length > MAX_MEMBERS_TO_RENDER
},

// Decode HTML entities in the circle display name so apostrophes (') and other
// HTML-encoded chars (e.g. &#39;) are shown correctly in the picker labels.
decodedTeamName(): string {
Expand Down Expand Up @@ -168,6 +243,24 @@ export default defineComponent({
},
},

watch: {
searchQueryDebounced(value) {
this.fetchCircleMembers()
},

searchRole(value) {
this.fetchCircleMembers()
},

'circle.id': {
handler() {
this.fetchCircleMembers()
},

immediate: true,
},
},

mounted() {
subscribe('contacts:circles:append', this.onShowPicker)
subscribe('guests:user:created', this.onGuestCreated)
Expand All @@ -179,6 +272,8 @@ export default defineComponent({
},

methods: {
t,

/**
* Measure the circle details header height from the DOM
* and keep it updated via ResizeObserver.
Expand Down Expand Up @@ -296,6 +391,26 @@ export default defineComponent({
const results = await getSuggestions(guest.username, this.circle)
this.$refs.entityPicker.onClick(results[0])
},

async fetchCircleMembers() {
if (!this.circle?.canManageMembers) {
return
}

this.loadingList = true
const payload = { circleId: this.circle.id, search: this.searchQuery || null, role: this.searchRole?.id }
this.logger.debug('Fetching members for', payload)

try {
await this.$store.dispatch('getCircleMembers', payload)
console.log('debug: getCircleMembers', this.list)
} catch (error) {
console.error(error)
showError(t('contacts', 'There was an error fetching the member list'))
} finally {
this.loadingList = false
}
},
},
})
</script>
Expand Down
21 changes: 21 additions & 0 deletions src/components/MemberList/MemberListGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<script setup lang="ts">
import type Member from '../../models/member.ts'

import { t } from '@nextcloud/l10n'
import { NcEmptyContent } from '@nextcloud/vue'
import IconSearch from 'vue-material-design-icons/Magnify.vue'
import MemberListItem from './MemberListItem.vue'

defineProps<{
Expand All @@ -22,6 +25,17 @@ defineProps<{
class="member-list-group__heading">
{{ label }}
</h4>

<template v-if="!members.length">
<div style="margin-top: 2rem;">
<NcEmptyContent :name="t('contacts', 'No results found')">
<template #icon>
<IconSearch :size="20" />
</template>
</NcEmptyContent>
</div>
</template>

<ul :aria-labelledby="`member-list-group-${type}`" class="member-list-group__list">
<MemberListItem
v-for="member in members"
Expand All @@ -32,6 +46,13 @@ defineProps<{
</template>

<style scoped lang="scss">
#member-list-group-1 {
margin-top: 0;
padding-top: 0;
}
.member-list-group__list-extended {
max-height: 200px;
}
.member-list-group {
&__heading {
display: flex;
Expand Down
2 changes: 1 addition & 1 deletion src/components/MemberList/MemberListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export default {
// If we changed an owner, let's refresh the whole dataset to update all ownership & memberships
if (level === MemberLevels.OWNER) {
await this.$store.dispatch('getCircle', this.circle.id)
await this.$store.dispatch('getCircleMembers', this.circle.id)
await this.$store.dispatch('getCircleMembers', { circleId: this.circle.id })
return
}

Expand Down
2 changes: 2 additions & 0 deletions src/models/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,5 @@ export enum MemberStatus {
MEMBER = 'Member',
REQUESTING = 'Requesting',
}
// fixme: increase to 100+
export const MAX_MEMBERS_TO_RENDER = 4
16 changes: 14 additions & 2 deletions src/services/circles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,22 @@ export async function leaveCircle(circleId: string) {
* Get the circle members without the members
*
* @param circleId the circle id
* @param search the search query
* @param role the role
* @param limit the limit
* @return
*/
export async function getCircleMembers(circleId: string) {
const response = await axios.get(generateOcsUrl('apps/circles/circles/{circleId}/members', { circleId }))
export async function getCircleMembers(circleId: string, search?: string, role?: string, limit: number = 100) {
const response = await axios.get(
generateOcsUrl('apps/circles/circles/{circleId}/members', { circleId }),
{
params: {
search,
role,
limit,
},
},
)
return response.data.ocs.data
}

Expand Down
Loading
Loading