Skip to content
Open
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
10 changes: 3 additions & 7 deletions cypress/e2e/collective-members.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ describe('Collective members', function() {
beforeEach(function() {
cy.loginAs('bob')
cy.visit('apps/collectives')
cy.intercept('GET', '**/circles/circles/*/members?**').as('getCircleMembers')
cy.openCollectiveMenu('Members Collective')
cy.clickMenuButton('Manage members')
cy.wait('@getCircleMembers')
})

it('Allows to add members', function() {
Expand All @@ -35,12 +37,10 @@ describe('Collective members', function() {
.find('.member-row__actions')
.click()
cy.intercept('PUT', '**/circles/circles/*/members/*/level').as('updateCircleMemberLevel')
cy.intercept('GET', '**/circles/circles/*/members?**').as('getCircleMembers')
cy.get('button.action-button')
.contains('Promote to admin')
.click()
cy.wait('@updateCircleMemberLevel')
cy.wait('@getCircleMembers')
cy.get('.current-members').contains('.member-row', member)
.should('contain', '(admin)')

Expand All @@ -49,12 +49,10 @@ describe('Collective members', function() {
.find('.member-row__actions')
.click()
cy.intercept('PUT', '**/circles/circles/*/members/*/level').as('updateCircleMemberLevel')
cy.intercept('GET', '**/circles/circles/*/members?**').as('getCircleMembers')
cy.get('button.action-button')
.contains('Demote to moderator')
.click()
cy.wait('@updateCircleMemberLevel')
cy.wait('@getCircleMembers')
cy.get('.current-members').contains('.member-row', member)
.should('contain', '(moderator)')
})
Expand All @@ -66,13 +64,11 @@ describe('Collective members', function() {
.find('.member-row__actions')
.click()
cy.intercept('DELETE', '**/circles/circles/*/members/*').as('removeCircleMember')
cy.intercept('GET', '**/circles/circles/*/members?**').as('getCircleMembers')
cy.get('button.action-button')
.contains('Remove')
.click()
cy.wait('@removeCircleMember')
cy.wait('@getCircleMembers')

cy.get('.current-members .member-row').should('not.contain', member)
cy.get('.current-members .member-row:visible').should('not.contain', member)
})
})
8 changes: 2 additions & 6 deletions src/components/Member/MemberItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@ export default {
t,

...mapActions(useCirclesStore, [
'getCircleMembers',
'changeCircleMemberLevel',
'removeMemberFromCircle',
]),
Expand All @@ -267,9 +266,8 @@ export default {
if (this.circleId) {
this.isLoadingLevel = true
await this.changeCircleMemberLevel({ circleId: this.circleId, memberId: this.memberId, level })
.then(async () => {
.then(() => {
showSuccess(t('collectives', 'Member level changed'))
await this.getCircleMembers(this.circleId)
}).catch((error) => {
showError(t('collectives', 'Could not change member level'))
throw error
Expand All @@ -283,16 +281,14 @@ export default {
if (this.circleId) {
this.isLoadingLevel = true
await this.removeMemberFromCircle({ circleId: this.circleId, memberId: this.memberId })
.then(async () => {
.then(() => {
showSuccess(t('collectives', 'Member removed'))
await this.getCircleMembers(this.circleId)
}).catch((error) => {
showError(t('collectives', 'Could not remove member'))
throw error
}).finally(() => {
this.isLoadingLevel = false
})
this.isLoadingLevel = false
}
},

Expand Down
7 changes: 6 additions & 1 deletion src/components/Member/MemberPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export default {
},

props: {
isLoading: {
type: Boolean,
default: false,
},

searchWithoutQuery: {
type: Boolean,
default: false,
Expand Down Expand Up @@ -169,7 +174,7 @@ export default {
},

showCurrentSkeleton() {
return this.showCurrent && this.currentMembers.length === 0
return this.showCurrent && (this.currentMembers.length === 0 || this.isLoading)
},
},

Expand Down
8 changes: 6 additions & 2 deletions src/components/Nav/CollectiveMembersModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
:circleId="collective.circleId"
:currentUserIsAdmin
:currentMembers="circleMembersSorted(collective.circleId)"
:isLoading="isLoadingMembers"
:onClickSearched />
</div>
</NcModal>
Expand Down Expand Up @@ -63,7 +64,7 @@ export default {
],

computed: {
...mapState(useCirclesStore, ['circleMembersSorted']),
...mapState(useCirclesStore, ['circleMembersSorted', 'circleMembersFullyLoaded']),
...mapState(useCollectivesStore, ['isCollectiveAdmin']),
...mapState(useRootStore, ['isPublic']),

Expand All @@ -82,6 +83,10 @@ export default {
showTeamLink() {
return this.hasContactsApp && !this.isPublic
},

isLoadingMembers() {
return !this.circleMembersFullyLoaded(this.collective.circleId)
},
},

beforeMount() {
Expand All @@ -107,7 +112,6 @@ export default {
userId: member.id,
type: circlesMemberTypes[autocompleteSourcesToCircleMemberTypes[member.source]],
})
await this.getCircleMembers(this.collective.circleId)
},
},
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Page/LandingPageWidgets/MembersWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import AccountMultiplePlusIcon from 'vue-material-design-icons/AccountMultiplePl
import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue'
import SkeletonLoading from '../../SkeletonLoading.vue'
import WidgetHeading from './WidgetHeading.vue'
import { circlesMemberTypes } from '../../../constants.js'
import { CIRCLE_MEMBERS_PARTIAL_LIMIT, circlesMemberTypes } from '../../../constants.js'
import { useCirclesStore } from '../../../stores/circles.js'
import { useCollectivesStore } from '../../../stores/collectives.js'
import { usePagesStore } from '../../../stores/pages.js'
Expand Down Expand Up @@ -208,7 +208,7 @@ export default {
const avatarHeight = defaultClickableArea + 12
if (membersWidth) {
const maxMembers = Math.floor(membersWidth / avatarHeight) - 1
this.showMembersCount = Math.min(this.sortedMembers.length, maxMembers)
this.showMembersCount = Math.min(this.sortedMembers.length, maxMembers, CIRCLE_MEMBERS_PARTIAL_LIMIT)
}
},

Expand Down
10 changes: 9 additions & 1 deletion src/components/Page/TextEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import SkeletonLoading from '../SkeletonLoading.vue'
import { useEditor } from '../../composables/useEditor.ts'
import { useReader } from '../../composables/useReader.ts'
import pageContentMixin from '../../mixins/pageContentMixin.js'
import { useCirclesStore } from '../../stores/circles.js'
import { useCollectivesStore } from '../../stores/collectives.js'
import { usePagesStore } from '../../stores/pages.js'
import { useRootStore } from '../../stores/root.js'
Expand Down Expand Up @@ -121,9 +122,15 @@ export default {
this.initEditMode()
})

this.textEditWatcher = this.$watch('isTextEdit', (val) => {
this.textEditWatcher = this.$watch('isTextEdit', async (val) => {
if (val === false) {
this.stopEdit()
} else if (val === true) {
// Load full circle members for autocomplete when entering edit mode
const circlesStore = useCirclesStore()
if (!circlesStore.currentCircleMembersFullyLoaded && !this.isPublic) {
await this.getCircleMembers(this.currentCollective.circleId)
}
}
})
subscribe('collectives:attachment:insert', this.insertAttachment)
Expand All @@ -144,6 +151,7 @@ export default {
...mapActions(useRootStore, ['load', 'done']),
...mapActions(useVersionsStore, ['getVersions']),
...mapActions(usePagesStore, ['setTextEdit', 'setTextPreview', 'touchPage']),
...mapActions(useCirclesStore, ['getCircleMembers']),

insertAttachment({ name }) {
// inspired by the fixedEncodeURIComponent function suggested in
Expand Down
3 changes: 3 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export const pageModes = {
MODE_EDIT: 1,
}

// Circle members
export const CIRCLE_MEMBERS_PARTIAL_LIMIT = 15

export const editorApiReaderFileId = 'READER_FILE_ID'
export const editorApiUpdateReadonlyBarProps = 'UPDATE_READONLY_BAR_PROPS'
export const editorApiAttachments = 'ATTACHMENTS'
Expand Down
43 changes: 39 additions & 4 deletions src/stores/circles.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const STORE_PREFIX = 'collectives/pinia/circles/'
export const useCirclesStore = defineStore('circles', {
state: () => ({
circles: useLocalStorage(STORE_PREFIX + 'circles', []),
circlesMembers: useLocalStorage(STORE_PREFIX + 'circlesMembers', {}),
circlesMembers: {},
circlesMembersFullyLoaded: {},
circlesMembersPending: {},
}),

getters: {
Expand Down Expand Up @@ -59,6 +61,14 @@ export const useCirclesStore = defineStore('circles', {
}
return users
},

circleMembersFullyLoaded: (state) => (circleId) => state.circlesMembersFullyLoaded[circleId] || false,

currentCircleMembersFullyLoaded: (state) => {
const collectivesStore = useCollectivesStore()
const currentCircleId = collectivesStore.currentCollective?.circleId
return state.circlesMembersFullyLoaded[currentCircleId] || false
},
},

actions: {
Expand Down Expand Up @@ -108,10 +118,23 @@ export const useCirclesStore = defineStore('circles', {
* Get members of a team
*
* @param {string} circleId ID of the team
* @param {number} limit Limit of members to fetch, 0 for all members
*/
async getCircleMembers(circleId) {
const response = await axios.get(generateOcsUrl(`apps/circles/circles/${circleId}/members?fullDetails=true`))
this.circlesMembers[circleId] = response.data.ocs.data
async getCircleMembers(circleId, limit = 0) {
// Skip if already fully loaded or request is pending
if (this.circlesMembersFullyLoaded[circleId] || this.circlesMembersPending[circleId]) {
return
}
this.circlesMembersPending[circleId] = true
try {
const response = await axios.get(generateOcsUrl(`apps/circles/circles/${circleId}/members?fullDetails=true&limit=${limit}`))

@Koc Koc Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fullDetails=true slowdowns endpoint by x4. And this needed only for detection is member is list current user or not

// CurrentMembers.vue
		isCurrentUser() {
			return (item) => {
				return item.userId === this.currentUser
					|| item.singleId === item.circle.initiator.singleId
			}
		},

We can think about improving that in follow-up PR

this.circlesMembers[circleId] = response.data.ocs.data
if (limit === 0) {
this.circlesMembersFullyLoaded[circleId] = true
}
} finally {
this.circlesMembersPending[circleId] = false
}
},

/**
Expand All @@ -127,6 +150,9 @@ export const useCirclesStore = defineStore('circles', {
generateOcsUrl('apps/circles/circles/' + circleId + '/members'),
{ userId, type },
)
if (this.circlesMembers[circleId]) {
this.circlesMembers[circleId].push(response.data.ocs.data)
}
return response.data.ocs.data
},

Expand Down Expand Up @@ -154,6 +180,9 @@ export const useCirclesStore = defineStore('circles', {
*/
async removeMemberFromCircle({ circleId, memberId }) {
const response = await axios.delete(generateOcsUrl('apps/circles/circles/' + circleId + '/members/' + memberId))
if (this.circlesMembers[circleId]) {
this.circlesMembers[circleId] = this.circlesMembers[circleId].filter((m) => m.id !== memberId)
}
return response.data.ocs.data
},

Expand All @@ -170,6 +199,12 @@ export const useCirclesStore = defineStore('circles', {
generateOcsUrl('apps/circles/circles/' + circleId + '/members/' + memberId + '/level'),
{ level },
)
if (this.circlesMembers[circleId]) {
const member = this.circlesMembers[circleId].find((m) => m.id === memberId)
if (member) {
member.level = level
}
}
return response.data.ocs.data
},

Expand Down
5 changes: 3 additions & 2 deletions src/views/CollectiveView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import CollectiveContainer from '../components/CollectiveContainer.vue'
import CollectiveNotFound from '../components/CollectiveNotFound.vue'
import PageList from '../components/PageList.vue'
import { useNetworkState } from '../composables/useNetworkState.ts'
import { sessionUpdateInterval } from '../constants.js'
import { CIRCLE_MEMBERS_PARTIAL_LIMIT, pageModes, sessionUpdateInterval } from '../constants.js'
import { useCirclesStore } from '../stores/circles.js'
import { useCollectivesStore } from '../stores/collectives.js'
import { usePagesStore } from '../stores/pages.js'
Expand Down Expand Up @@ -221,7 +221,8 @@ export default {
promises.push(this.getTemplates(setLoading))
}
if (!this.isPublic) {
promises.push(this.getCircleMembers(this.currentCollective.circleId))
const limit = this.currentCollective.pageMode === pageModes.MODE_EDIT ? 0 : CIRCLE_MEMBERS_PARTIAL_LIMIT
promises.push(this.getCircleMembers(this.currentCollective.circleId, limit))
}
}

Expand Down
Loading