diff --git a/frontend/src/modules/member/components/bulk/bulk-edit-attribute-dropdown.vue b/frontend/src/modules/member/components/bulk/bulk-edit-attribute-dropdown.vue new file mode 100644 index 0000000000..dc1f508f39 --- /dev/null +++ b/frontend/src/modules/member/components/bulk/bulk-edit-attribute-dropdown.vue @@ -0,0 +1,126 @@ + + + + + + + diff --git a/frontend/src/modules/member/components/bulk/bulk-edit-attribute-popover.vue b/frontend/src/modules/member/components/bulk/bulk-edit-attribute-popover.vue new file mode 100644 index 0000000000..a5fb4db2c3 --- /dev/null +++ b/frontend/src/modules/member/components/bulk/bulk-edit-attribute-popover.vue @@ -0,0 +1,382 @@ + + + + + + + diff --git a/frontend/src/modules/member/components/list/member-list-toolbar.vue b/frontend/src/modules/member/components/list/member-list-toolbar.vue index 9d2777cc3b..5144cb7156 100644 --- a/frontend/src/modules/member/components/list/member-list-toolbar.vue +++ b/frontend/src/modules/member/components/list/member-list-toolbar.vue @@ -68,6 +68,13 @@ /> {{ markAsTeamMemberOptions.copy }} + + + Edit attribute + + + @@ -119,6 +131,7 @@ import { getEnrichmentMax, showEnrichmentLoadingMessage, } from '@/modules/member/member-enrichment'; +import AppBulkEditAttributePopover from '@/modules/member/components/bulk/bulk-edit-attribute-popover.vue'; import AppTagPopover from '@/modules/tag/components/tag-popover.vue'; import AppSvg from '@/shared/svg/svg.vue'; @@ -129,6 +142,7 @@ const { selectedMembers, filters } = storeToRefs(memberStore); const { fetchMembers, getMemberCustomAttributes } = memberStore; const bulkTagsUpdateVisible = ref(false); +const bulkAttributesUpdateVisible = ref(false); const isReadOnly = computed(() => ( new MemberPermissions( @@ -285,6 +299,10 @@ const handleDoExport = async () => { } }; +const handleEditAttribute = async () => { + bulkAttributesUpdateVisible.value = true; +}; + const handleAddTags = async () => { bulkTagsUpdateVisible.value = true; }; @@ -315,6 +333,8 @@ const handleCommand = async (command) => { await handleDoExport(); } else if (command.action === 'mergeMembers') { await handleMergeMembers(); + } else if (command.action === 'editAttribute') { + await handleEditAttribute(); } else if (command.action === 'editTags') { await handleAddTags(); } else if (command.action === 'destroyAll') { diff --git a/frontend/src/modules/member/store/actions.js b/frontend/src/modules/member/store/actions.js index afd713fe12..4c13beb1d0 100644 --- a/frontend/src/modules/member/store/actions.js +++ b/frontend/src/modules/member/store/actions.js @@ -117,6 +117,75 @@ export default { } }, + async doBulkUpdateMembersAttribute({ commit }, { members, attributesToSave }) { + const { fields } = MemberModel; + const formSchema = new FormSchema([ + fields.info, + fields.joinedAt, + fields.organizations, + fields.attributes, + ]); + + try { + const payload = members.map((item) => { + const memberToUpdate = { ...item }; + + // 1. Update joinedAt + if (attributesToSave.joinedAt) { + memberToUpdate.joinedAt = attributesToSave.joinedAt; + } + + // 2. Append Organizations + if (attributesToSave.organizations) { + const orgIdsInMember = memberToUpdate.organizations.map((org) => org.id); + attributesToSave.organizations.forEach((org) => { + // Only append if org is not already in member + if (!orgIdsInMember.includes(org.id)) { + memberToUpdate.organizations.push(org); + } + }); + } + + // 3. Update attributes + if (attributesToSave.attributes) { + Object.keys(attributesToSave.attributes).forEach((attributeName) => { + const attributeValue = attributesToSave.attributes[attributeName]; + + // If the attribute value is an array, then append the values and not overwrite them + if (attributeValue && Array.isArray(attributeValue.default)) { + memberToUpdate.attributes[attributeName] = memberToUpdate.attributes[attributeName] || { default: [] }; + + // Get existing values of member attribute + const existingValues = memberToUpdate.attributes[attributeName].default; + + // Append only the new values to the member attribute and not the existing ones + const newValues = attributeValue.default.filter((value) => !existingValues.includes(value)); + memberToUpdate.attributes[attributeName].default.push(...newValues); + } else if (attributeValue && typeof attributeValue.default !== 'undefined') { + memberToUpdate.attributes[attributeName] = { default: attributeValue.default }; + } + }); + } + + return formSchema.cast({ + id: memberToUpdate.id, + joinedAt: memberToUpdate.joinedAt, + organizations: memberToUpdate.organizations, + attributes: memberToUpdate.attributes, + }); + }); + + const updatedMembers = await MemberService.updateBulk(payload); + + Message.success('Attribute updated successfully'); + + commit('UPDATE_SUCCESS', updatedMembers); + } catch (error) { + Errors.handle(error); + Message.error('There was an error updating attribute'); + } + }, + async doEnrich({ commit, dispatch, rootGetters }, id) { try { const currentTenant = rootGetters['auth/currentTenant'];