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 @@
+
+
+
+
+
+ Edit contact
+
+
+ {{ mergeSuggestionsCount }}Merge suggestion
+
+
+
+ Merge
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
Find GitHub