diff --git a/backend/src/api/tenant/tenantUpdate.ts b/backend/src/api/tenant/tenantUpdate.ts index 78f01ee1de..87eb731b7a 100644 --- a/backend/src/api/tenant/tenantUpdate.ts +++ b/backend/src/api/tenant/tenantUpdate.ts @@ -1,5 +1,6 @@ import { Error403 } from '@crowd/common' import TenantService from '../../services/tenantService' +import identifyTenant from '@/segment/identifyTenant' export default async (req, res) => { if (!req.currentUser || !req.currentUser.id) { @@ -10,5 +11,7 @@ export default async (req, res) => { // checked inside the service const payload = await new TenantService(req).update(req.params.id, req.body) + identifyTenant({ ...req, currentTenant: payload }) + await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/database/migrations/U1704395824__memberMergeSuggestionsLastGeneratedAt.sql b/backend/src/database/migrations/U1704395824__memberMergeSuggestionsLastGeneratedAt.sql new file mode 100644 index 0000000000..87c5849f6d --- /dev/null +++ b/backend/src/database/migrations/U1704395824__memberMergeSuggestionsLastGeneratedAt.sql @@ -0,0 +1 @@ +alter table "tenants" drop column "memberMergeSuggestionsLastGeneratedAt"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1704395824__memberMergeSuggestionsLastGeneratedAt.sql b/backend/src/database/migrations/V1704395824__memberMergeSuggestionsLastGeneratedAt.sql new file mode 100644 index 0000000000..0d89f2095c --- /dev/null +++ b/backend/src/database/migrations/V1704395824__memberMergeSuggestionsLastGeneratedAt.sql @@ -0,0 +1,2 @@ +alter table "tenants" +add column "memberMergeSuggestionsLastGeneratedAt" timestamp with time zone null; \ No newline at end of file diff --git a/backend/src/database/repositories/memberRepository.ts b/backend/src/database/repositories/memberRepository.ts index 4d3fd07833..d739f3f93a 100644 --- a/backend/src/database/repositories/memberRepository.ts +++ b/backend/src/database/repositories/memberRepository.ts @@ -238,44 +238,49 @@ class MemberRepository { } static async findMembersWithMergeSuggestions( - { limit = 20, offset = 0 }, + { limit = 20, offset = 0, memberId = undefined }, options: IRepositoryOptions, ) { - const currentTenant = SequelizeRepository.getCurrentTenant(options) const segmentIds = SequelizeRepository.getSegmentIds(options) + const isSegmentsEnabled = await isFeatureEnabled(FeatureFlag.SEGMENTS, options) + + const order = isSegmentsEnabled + ? 'mtm."activityEstimate" desc, mtm.similarity desc, mtm."memberId", mtm."toMergeId"' + : 'mtm.similarity desc, mtm."activityEstimate" desc, mtm."memberId", mtm."toMergeId"' + + const similarityFilter = isSegmentsEnabled ? ' and mtm.similarity > 0.95 ' : '' + + const memberFilter = memberId + ? ` and (mtm."memberId" = :memberId OR mtm."toMergeId" = :memberId)` + : '' + const mems = await options.database.sequelize.query( - `SELECT - "membersToMerge".id, - "membersToMerge"."toMergeId", - "membersToMerge"."total_count", - "membersToMerge"."similarity", - "membersToMerge"."activityEstimate" - FROM - ( - SELECT DISTINCT ON (Greatest(Hashtext(Concat(mem.id, mtm."toMergeId")), Hashtext(Concat(mtm."toMergeId", mem.id)))) - mem.id, - mtm."toMergeId", - mem."joinedAt", - COUNT(*) OVER() AS total_count, - mtm."similarity", - mtm."activityEstimate" - FROM members mem - INNER JOIN "memberToMerge" mtm ON mem.id = mtm."memberId" - JOIN "memberSegments" ms ON ms."memberId" = mem.id - WHERE mem."tenantId" = :tenantId - AND ms."segmentId" IN (:segmentIds) - ) AS "membersToMerge" - ORDER BY - "membersToMerge"."activityEstimate", "membersToMerge"."similarity" DESC - LIMIT :limit OFFSET :offset + ` + select + mtm."memberId" AS id, + mtm."toMergeId", + count(*) over() AS total_count, + mtm.similarity + from + "memberToMerge" mtm + where exists ( + select 1 + from "memberSegments" ms + where ms."segmentId" in (:segmentIds) and ms."memberId" = mtm."memberId" + ${memberFilter} + ) + ${similarityFilter} + order by ${order} + limit :limit + offset :offset; `, { replacements: { - tenantId: currentTenant.id, segmentIds, limit, offset, + memberId, }, type: QueryTypes.SELECT, }, @@ -297,7 +302,7 @@ class MemberRepository { members: [i, memberToMergeResults[idx]], similarity: mems[idx].similarity, })) - return { rows: result, count: mems[0].total_count / 2, limit, offset } + return { rows: result, count: mems[0].total_count, limit, offset } } return { rows: [{ members: [], similarity: 0 }], count: 0, limit, offset } diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index eb4f050537..3850107c99 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -1445,12 +1445,16 @@ class OrganizationRepository { } static async findOrganizationsWithMergeSuggestions( - { limit = 20, offset = 0 }, + { limit = 20, offset = 0, organizationId = undefined }, options: IRepositoryOptions, ) { const currentTenant = SequelizeRepository.getCurrentTenant(options) const segmentIds = SequelizeRepository.getSegmentIds(options) + const organizationFilter = organizationId + ? ` AND ("otm"."organizationId" = :organizationId OR "otm"."toMergeId" = :organizationId)` + : '' + const orgs = await options.database.sequelize.query( `WITH cte AS ( @@ -1473,6 +1477,7 @@ class OrganizationRepository { WHERE org."tenantId" = :tenantId AND os."segmentId" IN (:segmentIds) AND (ma.id IS NULL OR ma.state = :mergeActionStatus) + ${organizationFilter} ), count_cte AS ( @@ -1510,6 +1515,7 @@ class OrganizationRepository { offset, mergeActionType: MergeActionType.ORG, mergeActionStatus: MergeActionState.ERROR, + organizationId, }, type: QueryTypes.SELECT, }, diff --git a/backend/src/segment/identifyTenant.ts b/backend/src/segment/identifyTenant.ts index 5456c8a20f..641d6461e9 100644 --- a/backend/src/segment/identifyTenant.ts +++ b/backend/src/segment/identifyTenant.ts @@ -11,9 +11,12 @@ export default async function identifyTenant(req) { analytics.group({ userId: req.currentUser.id, groupId: req.currentTenant.id, - traits: { - name: req.currentTenant.name, - }, + traits: + req.currentTenant.name !== 'temporaryName' + ? { + name: req.currentTenant.name, + } + : undefined, }) } } else if (API_CONFIG.edition === Edition.COMMUNITY) { diff --git a/backend/src/services/__tests__/tenantService.test.ts b/backend/src/services/__tests__/tenantService.test.ts index 31cd532b6e..9d45442d96 100644 --- a/backend/src/services/__tests__/tenantService.test.ts +++ b/backend/src/services/__tests__/tenantService.test.ts @@ -24,97 +24,6 @@ describe('TenantService tests', () => { await SequelizeTestUtils.closeConnection(db) }) - describe('findMembersToMerge', () => { - it('Should show the same merge suggestion once, with reverse order', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const memberService = new MemberService(mockIServiceOptions) - const tenantService = new TenantService(mockIServiceOptions) - - const memberToCreate1 = { - username: { - [PlatformType.SLACK]: { - username: 'member 1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.SLACK, - email: 'member.1@email.com', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberToCreate2 = { - username: { - [PlatformType.DISCORD]: { - username: 'member 2', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.DISCORD, - email: 'member.2@email.com', - joinedAt: '2020-05-26T15:13:30Z', - } - - const memberToCreate3 = { - username: { - [PlatformType.GITHUB]: { - username: 'member 3', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - email: 'member.3@email.com', - joinedAt: '2020-05-25T15:13:30Z', - } - - const memberToCreate4 = { - username: { - [PlatformType.TWITTER]: { - username: 'member 4', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.TWITTER, - email: 'member.4@email.com', - joinedAt: '2020-05-24T15:13:30Z', - } - - const member1 = await memberService.upsert(memberToCreate1) - let member2 = await memberService.upsert(memberToCreate2) - const member3 = await memberService.upsert(memberToCreate3) - let member4 = await memberService.upsert(memberToCreate4) - - await memberService.addToMerge([{ members: [member1.id, member2.id], similarity: 1 }]) - await memberService.addToMerge([{ members: [member3.id, member4.id], similarity: 0.5 }]) - - member2 = await memberService.findById(member2.id) - member4 = await memberService.findById(member4.id) - - const memberToMergeSuggestions = await tenantService.findMembersToMerge({}) - - // In the DB there should be: - // - Member 1 should have member 2 in toMerge - // - Member 3 should have member 4 in toMerge - // - Member 4 should have member 3 in toMerge - // - We should get these 4 combinations - // But this function should not return duplicates, so we should get - // only two pairs: [m2, m1] and [m4, m3] - - expect(memberToMergeSuggestions.count).toEqual(1) - - expect( - memberToMergeSuggestions.rows[0].members - .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1)) - .map((m) => m.id), - ).toStrictEqual([member1.id, member2.id]) - - expect( - memberToMergeSuggestions.rows[1].members - .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1)) - .map((m) => m.id), - ).toStrictEqual([member3.id, member4.id]) - }) - }) - describe('_findAndCountAllForEveryUser method', () => { it('Should succesfully find all tenants without filtering by currentUser', async () => { let tenants = await TenantService._findAndCountAllForEveryUser({ filter: {} }) diff --git a/frontend/src/modules/member/components/member-actions.vue b/frontend/src/modules/member/components/member-actions.vue new file mode 100644 index 0000000000..de658eaf28 --- /dev/null +++ b/frontend/src/modules/member/components/member-actions.vue @@ -0,0 +1,135 @@ + + + + + + + diff --git a/frontend/src/modules/member/components/member-dropdown-content.vue b/frontend/src/modules/member/components/member-dropdown-content.vue index 93f564a872..18132c6d15 100644 --- a/frontend/src/modules/member/components/member-dropdown-content.vue +++ b/frontend/src/modules/member/components/member-dropdown-content.vue @@ -1,5 +1,6 @@