diff --git a/backend/src/api/member/memberFind.ts b/backend/src/api/member/memberFind.ts index e95dfc4cfe..7d6de9b14e 100644 --- a/backend/src/api/member/memberFind.ts +++ b/backend/src/api/member/memberFind.ts @@ -1,8 +1,8 @@ -import { isFeatureEnabled } from '@crowd/feature-flags' import { FeatureFlag } from '@crowd/types' import Permissions from '../../security/permissions' import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' +import isFeatureEnabled from '../../feature-flags/isFeatureEnabled' /** * GET /tenant/{tenantId}/member/{id} @@ -34,7 +34,12 @@ export default async (req, res) => { } } - const payload = await new MemberService(req).findById(req.params.id, true, true, segmentId) + let payload + if (await isFeatureEnabled(FeatureFlag.SERVE_PROFILES_OPENSEARCH, req)) { + payload = await new MemberService(req).findByIdOpensearch(req.params.id, segmentId) + } else { + payload = await new MemberService(req).findById(req.params.id, true, true, segmentId) + } await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/organization/organizationFind.ts b/backend/src/api/organization/organizationFind.ts index 79888a0d38..a45ad7df12 100644 --- a/backend/src/api/organization/organizationFind.ts +++ b/backend/src/api/organization/organizationFind.ts @@ -1,8 +1,8 @@ import { FeatureFlag } from '@crowd/types' -import isFeatureEnabled from '@/feature-flags/isFeatureEnabled' import Permissions from '../../security/permissions' import OrganizationService from '../../services/organizationService' import PermissionChecker from '../../services/user/permissionChecker' +import isFeatureEnabled from '../../feature-flags/isFeatureEnabled' /** * GET /tenant/{tenantId}/organization/{id} @@ -34,7 +34,12 @@ export default async (req, res) => { } } - const payload = await new OrganizationService(req).findById(req.params.id, segmentId) + let payload + if (await isFeatureEnabled(FeatureFlag.SERVE_PROFILES_OPENSEARCH, req)) { + payload = await new OrganizationService(req).findByIdOpensearch(req.params.id, segmentId) + } else { + payload = await new OrganizationService(req).findById(req.params.id, segmentId) + } await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/premium/enrichment/memberEnrich.ts b/backend/src/api/premium/enrichment/memberEnrich.ts index 73488b9438..abe1bbd663 100644 --- a/backend/src/api/premium/enrichment/memberEnrich.ts +++ b/backend/src/api/premium/enrichment/memberEnrich.ts @@ -1,6 +1,6 @@ import { RedisCache } from '@crowd/redis' import { getServiceLogger } from '@crowd/logging' -import { FeatureFlagRedisKey } from '@crowd/types' +import { FeatureFlagRedisKey, SyncMode } from '@crowd/types' import { getSecondsTillEndOfMonth } from '../../../utils/timing' import Permissions from '../../../security/permissions' import identifyTenant from '../../../segment/identifyTenant' @@ -29,7 +29,10 @@ const log = getServiceLogger() export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberEdit) - const payload = await new MemberEnrichmentService(req).enrichOne(req.params.id) + const payload = await new MemberEnrichmentService(req).enrichOne( + req.params.id, + SyncMode.SYNCHRONOUS, + ) track('Single member enrichment', { memberId: req.params.id }, { ...req }) diff --git a/backend/src/bin/scripts/unleash-init.ts b/backend/src/bin/scripts/unleash-init.ts index 18f37d25b2..1c498321d5 100644 --- a/backend/src/bin/scripts/unleash-init.ts +++ b/backend/src/bin/scripts/unleash-init.ts @@ -273,6 +273,24 @@ const constaintConfiguration = { }, ], ], + + [FeatureFlag.SERVE_PROFILES_OPENSEARCH]: [ + [ + { + values: [ + Plans.values.scale, + Plans.values.eagleEye, + Plans.values.enterprise, + Plans.values.essential, + Plans.values.growth, + ], + inverted: false, + operator: 'IN', + contextName: 'plan', + caseInsensitive: false, + }, + ], + ], } let seq: any diff --git a/backend/src/database/repositories/activityRepository.ts b/backend/src/database/repositories/activityRepository.ts index 9f5581e9b6..06e5373339 100644 --- a/backend/src/database/repositories/activityRepository.ts +++ b/backend/src/database/repositories/activityRepository.ts @@ -677,7 +677,10 @@ class ActivityRepository { offset, }) - let rows = await options.database.activity.findAll({ + let { + rows, + count, // eslint-disable-line prefer-const + } = await options.database.activity.findAndCountAll({ include, attributes: [ ...SequelizeFilterUtils.getLiteralProjectionsOfModel('activity', options.database), @@ -692,21 +695,7 @@ class ActivityRepository { rows = await this._populateRelationsForRows(rows, options) - const [countRow] = await options.database.sequelize.query( - ` - SELECT n_live_tup AS count - FROM pg_stat_all_tables - WHERE schemaname = 'public' - AND relname = 'activities' - `, - { - type: Sequelize.QueryTypes.SELECT, - transaction: SequelizeRepository.getTransaction(options), - }, - ) - const { count } = countRow - - return { rows, count: parseInt(count, 10), limit: parsed.limit, offset: parsed.offset } + return { rows, count, limit: parsed.limit, offset: parsed.offset } } static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { diff --git a/backend/src/database/repositories/memberRepository.ts b/backend/src/database/repositories/memberRepository.ts index 2e0ca4bfb7..5d002dc9dc 100644 --- a/backend/src/database/repositories/memberRepository.ts +++ b/backend/src/database/repositories/memberRepository.ts @@ -48,6 +48,7 @@ import { import OrganizationRepository from './organizationRepository' import MemberSyncRemoteRepository from './memberSyncRemoteRepository' import MemberAffiliationRepository from './memberAffiliationRepository' +import MemberAttributeSettingsRepository from './memberAttributeSettingsRepository' const { Op } = Sequelize @@ -1162,6 +1163,77 @@ class MemberRepository { }) } + static async findByIdOpensearch(id, options: IRepositoryOptions, segmentId?: string) { + const segments = segmentId ? [segmentId] : SequelizeRepository.getSegmentIds(options) + + const memberAttributeSettings = ( + await MemberAttributeSettingsRepository.findAndCountAll({}, options) + ).rows + + const response = await this.findAndCountAllOpensearch( + { + filter: { + and: [ + { + id: { + eq: id, + }, + }, + ], + }, + limit: 1, + offset: 0, + attributesSettings: memberAttributeSettings, + segments, + }, + options, + ) + + if (response.count === 0) { + throw new Error404() + } + + const result = response.rows[0] + + // Get special attributes from memberAttributeSettings + const specialAttributes = memberAttributeSettings + .filter((setting) => setting.type === 'special') + .map((setting) => setting.name) + + // Parse special attributes that are indexed as strings + if (result.attributes) { + specialAttributes.forEach((attr) => { + if (result.attributes[attr]) { + result.attributes[attr] = JSON.parse(result.attributes[attr]) + } + }) + } + + // Sort the organizations based on dateStart + if (result.organizations) { + result.organizations.sort((a, b) => { + const dateStartA = a.memberOrganizations.dateStart + const dateStartB = b.memberOrganizations.dateStart + + if (!dateStartA && !dateStartB) { + return 0 + } + + if (!dateStartA) { + return 1 + } + + if (!dateStartB) { + return -1 + } + + return new Date(dateStartB).getTime() - new Date(dateStartA).getTime() + }) + } + + return result + } + static async findAndCountActiveOpensearch( filter: IActiveMemberFilter, limit: number, diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index abbf63ee34..2b89f960fe 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -2132,6 +2132,46 @@ class OrganizationRepository { return results } + static async findByIdOpensearch( + id: string, + options: IRepositoryOptions, + segmentId?: string, + ): Promise> { + const segments = segmentId ? [segmentId] : SequelizeRepository.getSegmentIds(options) + + const response = await this.findAndCountAllOpensearch( + { + filter: { + and: [ + { + id: { + eq: id, + }, + }, + ], + }, + isProfileQuery: true, + limit: 1, + offset: 0, + segments, + }, + options, + ) + + if (response.count === 0) { + throw new Error404() + } + + const result = response.rows[0] + + // Parse attributes that are indexed as strings + if (result.attributes) { + result.attributes = JSON.parse(result.attributes) + } + + return result + } + static async findAndCountAllOpensearch( { filter = {} as any, @@ -2141,6 +2181,7 @@ class OrganizationRepository { countOnly = false, segments = [] as string[], customSortFunction = undefined, + isProfileQuery = false, }, options: IRepositoryOptions, ): Promise> { @@ -2156,7 +2197,7 @@ class OrganizationRepository { const translator = FieldTranslatorFactory.getTranslator(OpenSearchIndex.ORGANIZATIONS) - if (filter.and) { + if (!isProfileQuery && filter.and) { filter.and.push({ or: [ { diff --git a/backend/src/serverless/microservices/nodejs/org-merge/orgMergeWorker.ts b/backend/src/serverless/microservices/nodejs/org-merge/orgMergeWorker.ts index 747927ddd2..7ea6f6c062 100644 --- a/backend/src/serverless/microservices/nodejs/org-merge/orgMergeWorker.ts +++ b/backend/src/serverless/microservices/nodejs/org-merge/orgMergeWorker.ts @@ -4,7 +4,15 @@ import { REDIS_CONFIG } from '../../../../conf' import getUserContext from '../../../../database/utils/getUserContext' import OrganizationService from '../../../../services/organizationService' -async function doNotifyFrontend({ log, success, tenantId, primaryOrgId, secondaryOrgId }) { +async function doNotifyFrontend({ + log, + success, + tenantId, + primaryOrgId, + secondaryOrgId, + original, + toMerge, +}) { const redis = await getRedisClient(REDIS_CONFIG, true) const apiPubSubEmitter = new RedisPubSubEmitter( 'api-pubsub', @@ -24,6 +32,8 @@ async function doNotifyFrontend({ log, success, tenantId, primaryOrgId, secondar tenantId, primaryOrgId, secondaryOrgId, + original, + toMerge, }), undefined, tenantId, @@ -42,8 +52,13 @@ async function orgMergeWorker( const organizationService = new OrganizationService(userContext) let success = true + let original + let toMerge try { - await organizationService.mergeSync(primaryOrgId, secondaryOrgId) + const response = await organizationService.mergeSync(primaryOrgId, secondaryOrgId) + + original = response.original + toMerge = response.toMerge } catch (err) { userContext.log.error(err, 'Error merging orgs') success = false @@ -56,6 +71,8 @@ async function orgMergeWorker( tenantId, primaryOrgId, secondaryOrgId, + original, + toMerge, }) } } diff --git a/backend/src/services/integrationService.ts b/backend/src/services/integrationService.ts index e085dacc68..c8dd8caed2 100644 --- a/backend/src/services/integrationService.ts +++ b/backend/src/services/integrationService.ts @@ -1201,12 +1201,18 @@ export default class IntegrationService { async gitConnectOrUpdate(integrationData) { const transaction = await SequelizeRepository.createTransaction(this.options) let integration + const stripGit = (url: string) => { + if (url.endsWith('.git')) { + return url.slice(0, -4) + } + return url + } try { integration = await this.createOrUpdate( { platform: PlatformType.GIT, settings: { - remotes: integrationData.remotes, + remotes: integrationData.remotes.map((remote) => stripGit(remote)), }, status: 'done', }, diff --git a/backend/src/services/memberService.ts b/backend/src/services/memberService.ts index 203dcb723c..9080ce7411 100644 --- a/backend/src/services/memberService.ts +++ b/backend/src/services/memberService.ts @@ -1131,6 +1131,10 @@ export default class MemberService extends LoggerBase { ) } + async findByIdOpensearch(id: string, segmentId?: string) { + return MemberRepository.findByIdOpensearch(id, this.options, segmentId) + } + async queryV2(data) { if (await isFeatureEnabled(FeatureFlag.SEGMENTS, this.options)) { if (data.segments.length !== 1) { diff --git a/backend/src/services/organizationService.ts b/backend/src/services/organizationService.ts index 4ca8e64d9c..e4e5eaba79 100644 --- a/backend/src/services/organizationService.ts +++ b/backend/src/services/organizationService.ts @@ -274,7 +274,12 @@ export default class OrganizationService extends LoggerBase { ) this.options.log.info({ originalId, toMergeId }, 'Organizations merged!') - return { status: 200, mergedId: originalId } + return { + status: 200, + mergedId: originalId, + original: original.displayName, + toMerge: toMerge.displayName, + } } catch (err) { this.options.log.error(err, 'Error while merging organizations!', { originalId, @@ -811,6 +816,10 @@ export default class OrganizationService extends LoggerBase { return OrganizationRepository.findOrCreateByDomain(domain, this.options) } + async findByIdOpensearch(id: string, segmentId?: string) { + return OrganizationRepository.findByIdOpensearch(id, this.options, segmentId) + } + async query(data) { const advancedFilter = data.filter const orderBy = data.orderBy diff --git a/backend/src/services/premium/enrichment/memberEnrichmentService.ts b/backend/src/services/premium/enrichment/memberEnrichmentService.ts index ba42cd974f..89138196e4 100644 --- a/backend/src/services/premium/enrichment/memberEnrichmentService.ts +++ b/backend/src/services/premium/enrichment/memberEnrichmentService.ts @@ -145,7 +145,6 @@ export default class MemberEnrichmentService extends LoggerBase { async bulkEnrich(memberIds: string[], notifyFrontend: boolean = true) { const redis = await getRedisClient(REDIS_CONFIG, true) - const searchSyncService = new SearchSyncService(this.options, SyncMode.ASYNCHRONOUS) const apiPubSubEmitter = new RedisPubSubEmitter( 'api-pubsub', @@ -160,7 +159,6 @@ export default class MemberEnrichmentService extends LoggerBase { try { await this.enrichOne(memberId) enrichedMembers++ - await searchSyncService.triggerMemberSync(this.options.currentTenant.id, memberId) this.log.info(`Enriched member ${memberId}`) } catch (err) { if ( @@ -224,12 +222,8 @@ export default class MemberEnrichmentService extends LoggerBase { * @param memberId - the ID of the member to enrich * @returns a promise that resolves to the enrichment data for the member */ - async enrichOne(memberId) { + async enrichOne(memberId, syncMode = SyncMode.ASYNCHRONOUS) { const transaction = await SequelizeRepository.createTransaction(this.options) - const options = { - ...this.options, - transaction, - } try { // If the attributes have not been fetched yet, fetch them @@ -237,10 +231,10 @@ export default class MemberEnrichmentService extends LoggerBase { await this.getAttributes() } - const searchSyncService = new SearchSyncService(this.options, SyncMode.ASYNCHRONOUS) + const searchSyncService = new SearchSyncService(this.options, syncMode) // Create an instance of the MemberService and use it to look up the member - const memberService = new MemberService(options) + const memberService = new MemberService(this.options) const member = await memberService.findById(memberId, false, false) // If the member's GitHub handle or email address is not available, throw an error @@ -290,7 +284,7 @@ export default class MemberEnrichmentService extends LoggerBase { // add the member to merge suggestions await MemberRepository.addToMerge( [{ similarity: 0.9, members: [memberId, existingMember.id] }], - options, + this.options, ) if (Array.isArray(normalized.username[platform])) { @@ -311,8 +305,7 @@ export default class MemberEnrichmentService extends LoggerBase { } // save raw data to cache - await MemberEnrichmentCacheRepository.upsert(memberId, enrichmentData, options) - + await MemberEnrichmentCacheRepository.upsert(memberId, enrichmentData, this.options) // We are updating the displayName only if the existing one has one word only // And we are using an update here instead of the upsert because // upsert always takes the existing displayName @@ -334,7 +327,7 @@ export default class MemberEnrichmentService extends LoggerBase { memberId: member.id, enrichedFrom, }, - options, + this.options, ) let result = await memberService.upsert( @@ -350,7 +343,7 @@ export default class MemberEnrichmentService extends LoggerBase { // for every work experience in `enrichmentData` // - upsert organization // - upsert `memberOrganization` relation - const organizationService = new OrganizationService(options) + const organizationService = new OrganizationService(this.options) if (enrichmentData.work_experiences) { for (const workExperience of enrichmentData.work_experiences) { const organizationIdentities: IOrganizationIdentity[] = [ @@ -374,7 +367,7 @@ export default class MemberEnrichmentService extends LoggerBase { }, { doSync: true, - mode: SyncMode.ASYNCHRONOUS, + mode: syncMode, }, ) @@ -390,15 +383,15 @@ export default class MemberEnrichmentService extends LoggerBase { dateEnd, source: OrganizationSource.ENRICHMENT, } - await MemberRepository.createOrUpdateWorkExperience(data, options) - await OrganizationRepository.includeOrganizationToSegments(org.id, options) + await MemberRepository.createOrUpdateWorkExperience(data, this.options) + await OrganizationRepository.includeOrganizationToSegments(org.id, this.options) } } + await SequelizeRepository.commitTransaction(transaction) await searchSyncService.triggerMemberSync(this.options.currentTenant.id, result.id) - result = await memberService.findById(result.id, true, false) - await SequelizeRepository.commitTransaction(transaction) + result = await MemberRepository.findByIdOpensearch(result.id, this.options) return result } catch (error) { this.log.error(error, 'Error while enriching a member!') @@ -523,7 +516,6 @@ export default class MemberEnrichmentService extends LoggerBase { // Assign 'value' to 'member.attributes[attributeName].enrichment' member.attributes[attributeName].enrichment = value - await this.createAttributeAndUpdateOptions(attributeName, attribute, value) } } diff --git a/frontend/public/icons/crowd-icons.svg b/frontend/public/icons/crowd-icons.svg index a5f94c5479..60af9719ba 100644 --- a/frontend/public/icons/crowd-icons.svg +++ b/frontend/public/icons/crowd-icons.svg @@ -262,6 +262,17 @@ + + + + + + + + + + + @@ -291,6 +302,12 @@ fill="#D1D5DB" /> + + + + + + diff --git a/frontend/src/integrations/custom/lfx/config.js b/frontend/src/integrations/custom/lfx/config.js index 0d9c5960a4..ea5bcd0df4 100644 --- a/frontend/src/integrations/custom/lfx/config.js +++ b/frontend/src/integrations/custom/lfx/config.js @@ -1,7 +1,10 @@ export default { - name: 'lfx', - image: '/images/integrations/custom/lfx.png', + name: "lfx", + image: "/images/integrations/custom/lfx.png", enabled: true, - chartColor: '#003764', + chartColor: "#003764", url: () => null, + supportUrl: + "https://jira.linuxfoundation.org/plugins/servlet/desk/portal/4?requestGroup=54", + hideCustomIntegration: true, }; diff --git a/frontend/src/modules/activity/components/activity-timeline.vue b/frontend/src/modules/activity/components/activity-timeline.vue index 4e3808ff93..b435b33e27 100644 --- a/frontend/src/modules/activity/components/activity-timeline.vue +++ b/frontend/src/modules/activity/components/activity-timeline.vue @@ -1,65 +1,84 @@
- - + + + + +
@@ -234,6 +253,8 @@ import AppActivityContentFooter from '@/modules/activity/components/activity-con import AppLfActivityParent from '@/modules/lf/activity/components/lf-activity-parent.vue'; import AppConversationDrawer from '@/modules/conversation/components/conversation-drawer.vue'; import AppActivityDropdown from '@/modules/activity/components/activity-dropdown.vue'; +import { storeToRefs } from 'pinia'; +import { useLfSegmentsStore } from '@/modules/lf/segments/store'; const SearchIcon = h( 'i', // type @@ -257,13 +278,16 @@ const props = defineProps({ }, }); +const lsSegmentsStore = useLfSegmentsStore(); +const { projectGroups } = storeToRefs(lsSegmentsStore); + const conversationId = ref(null); const activeIntegrations = computed(() => { const activeIntegrationList = store.getters['integration/activeList']; return Object.keys(activeIntegrationList).map((i) => ({ ...activeIntegrationList[i], - label: CrowdIntegrations.getConfig(i).name, + label: CrowdIntegrations.getConfig(i)?.name, })); }); @@ -274,17 +298,31 @@ const activities = ref([]); const limit = ref(20); const offset = ref(0); const noMore = ref(false); +const selectedSegment = ref(null); let filter = {}; const isMemberEntity = computed(() => props.entityType === 'member'); +const subprojects = computed(() => projectGroups.value.list.reduce((acc, projectGroup) => { + projectGroup.projects.forEach((project) => { + project.subprojects.forEach((subproject) => { + acc[subproject.id] = { + id: subproject.id, + name: subproject.name, + }; + }); + }); + + return acc; +}, {})); + const segments = computed(() => props.entity.segments?.map((s) => { if (typeof s === 'string') { - return s; + return subprojects.value[s]; } - return s.id; + return s; }) || []); const fetchActivities = async ({ reset } = { reset: false }) => { @@ -358,7 +396,7 @@ const fetchActivities = async ({ reset } = { reset: false }) => { orderBy: 'timestamp_DESC', limit: limit.value, offset: offset.value, - segments: segments.value, + segments: selectedSegment.value ? [selectedSegment.value] : segments.value.map((s) => s.id), }, { headers: { @@ -404,7 +442,7 @@ watch(platform, async (newValue, oldValue) => { }); onMounted(async () => { - await store.dispatch('integration/doFetch', segments.value); + await store.dispatch('integration/doFetch', segments.value.map((s) => s.id)); await fetchActivities(); }); diff --git a/frontend/src/modules/auth/auth-socket.js b/frontend/src/modules/auth/auth-socket.js index 8e0a5184ff..04f3a5ff6c 100644 --- a/frontend/src/modules/auth/auth-socket.js +++ b/frontend/src/modules/auth/auth-socket.js @@ -1,5 +1,5 @@ import io from 'socket.io-client'; -import { computed } from 'vue'; +import { computed, h } from 'vue'; import pluralize from 'pluralize'; import config from '@/config'; import { store } from '@/store'; @@ -9,9 +9,20 @@ import { getEnrichmentMax, } from '@/modules/member/member-enrichment'; import { useMemberStore } from '@/modules/member/store/pinia'; +import { router } from '@/router'; +import { useOrganizationStore } from '@/modules/organization/store/pinia'; let socketIoClient; +const SocketEvents = { + connect: 'connect', + disconnect: 'disconnect', + integrationCompleted: 'integration-completed', + tenantPlanUpgraded: 'tenant-plan-upgraded', + bulkEnrichment: 'bulk-enrichment', + orgMerge: 'org-merge', +}; + export const connectSocket = (token) => { if (socketIoClient && socketIoClient.connected) { socketIoClient.disconnect(); @@ -36,15 +47,15 @@ export const connectSocket = (token) => { forceNew: true, }); - socketIoClient.on('connect', () => { + socketIoClient.on(SocketEvents.connect, () => { console.info('Socket connected'); }); - socketIoClient.on('disconnect', () => { + socketIoClient.on(SocketEvents.disconnect, () => { console.info('Socket disconnected'); }); - socketIoClient.on('integration-completed', (data) => { + socketIoClient.on(SocketEvents.integrationCompleted, (data) => { console.info('Integration onboarding done', data); store.dispatch( 'integration/doFind', @@ -53,7 +64,7 @@ export const connectSocket = (token) => { }); socketIoClient.on( - 'tenant-plan-upgraded', + SocketEvents.tenantPlanUpgraded, async (data) => { console.info( 'Tenant plan is upgraded. Force a hard refresh!', @@ -72,7 +83,7 @@ export const connectSocket = (token) => { }, ); - socketIoClient.on('bulk-enrichment', async (data) => { + socketIoClient.on(SocketEvents.bulkEnrichment, async (data) => { let parsed = data; if (typeof data === 'string') { parsed = JSON.parse(parsed); @@ -116,6 +127,73 @@ export const connectSocket = (token) => { } } }); + + socketIoClient.on(SocketEvents.orgMerge, (payload) => { + const { + success, + tenantId, + primaryOrgId, + secondaryOrgId, + original, + toMerge, + } = JSON.parse(payload); + + if (currentTenant.value.id !== tenantId) { + return; + } + + const { mergedOrganizations, removeMergedOrganizations } = useOrganizationStore(); + + const buttonElement = h( + 'el-button', + { + class: 'btn btn--xs btn--secondary !h-6 !w-fit', + onClick: () => { + router.push({ + name: 'organizationView', + params: { id: primaryOrgId }, + }); + Message.closeAll(); + }, + }, + 'View organization', + ); + + const messageElements = [buttonElement]; + + if (original && toMerge) { + const descriptionElement = h( + 'span', + { + innerHTML: `${toMerge} merged with ${original}.`, + }, + ); + + removeMergedOrganizations(primaryOrgId); + + messageElements.unshift(descriptionElement); + } + + Message.closeAll(); + + if (success) { + Message.success( + h( + 'div', + { + class: 'flex flex-col gap-2', + }, + messageElements, + ), + { + title: + 'Organizations merged successfully', + }, + ); + } else { + Message.error(`There was an error merging ${toMerge} with ${original}`); + } + }); }; export const disconnectSocket = () => { diff --git a/frontend/src/modules/integration/components/integration-list-page.vue b/frontend/src/modules/integration/components/integration-list-page.vue index d20d371c27..fa000f774c 100644 --- a/frontend/src/modules/integration/components/integration-list-page.vue +++ b/frontend/src/modules/integration/components/integration-list-page.vue @@ -10,7 +10,7 @@
🧐 Missing something? Open an issue
@@ -106,6 +106,7 @@ import ConfirmDialog from '@/shared/dialog/confirm-dialog'; import Message from '@/shared/message/message'; import AppIntegrationList from './integration-list.vue'; + export default { name: 'AppIntegrationListPage', diff --git a/frontend/src/modules/integration/components/integration-list.vue b/frontend/src/modules/integration/components/integration-list.vue index c2a770d179..2384ff29d2 100644 --- a/frontend/src/modules/integration/components/integration-list.vue +++ b/frontend/src/modules/integration/components/integration-list.vue @@ -16,6 +16,7 @@ :integration="integration" />
@@ -76,6 +77,8 @@ const integrationsArray = computed(() => (props.onboard const showGithubDialog = ref(false); +const hideCustomIntegrations = CrowdIntegrations.getConfig('lfx').hideCustomIntegration; + onMounted(async () => { localStorage.setItem('segmentId', route.params.id); localStorage.setItem('segmentGrandparentId', route.params.grandparentId); diff --git a/frontend/src/modules/member/components/list/member-list-table.vue b/frontend/src/modules/member/components/list/member-list-table.vue index 4a3b02e586..ed03675822 100644 --- a/frontend/src/modules/member/components/list/member-list-table.vue +++ b/frontend/src/modules/member/components/list/member-list-table.vue @@ -94,6 +94,8 @@ :row-class-name="rowClass" @sort-change="doChangeSort" @selection-change="selectedMembers = $event" + @cell-mouse-enter="handleCellMouseEnter" + @cell-mouse-leave="handleCellMouseLeave" > @@ -416,34 +418,48 @@ sortable="custom" > @@ -451,17 +467,24 @@ @@ -490,17 +521,24 @@ @@ -533,17 +579,24 @@ @@ -636,15 +697,14 @@ +
+ + + + + + currentTenant.value?.plan !== Plans.values.essential); const defaultSort = computed(() => ({ prop: filters.value.order.prop, @@ -838,6 +922,44 @@ const onActionBtnClick = (member) => { } }; +const setEnrichmentAttributesRef = (el, id) => { + if (el) { + enrichmentRefs.value[id] = el; + } +}; + +const handleCellMouseEnter = (row, column) => { + const validValues = ['reach', 'seniorityLevel', 'programmingLanguages', 'skills']; + + if (validValues.includes(column.property)) { + showEnrichmentPopover.value = true; + selectedEnrichmentAttribute.value = `${row.id}-${column.property}`; + } +}; + +const onColumnHeaderMouseOver = (id) => { + showEnrichmentPopover.value = true; + selectedEnrichmentAttribute.value = id; +}; + +const handleCellMouseLeave = (_row, column) => { + const validValues = ['reach', 'seniorityLevel', 'programmingLanguages', 'skills']; + + if (!validValues.includes(column.property)) { + closeEnrichmentPopover(); + } +}; + +const closeEnrichmentPopover = (ev) => { + if (ev?.toElement?.id !== 'popover-content') { + showEnrichmentPopover.value = false; + + setTimeout(() => { + selectedEnrichmentAttribute.value = null; + }, 100); + } +}; + const closeDropdown = () => { showMemberDropdownPopover.value = false; 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 bfd2fc4d70..26ee25f220 100644 --- a/frontend/src/modules/member/components/list/member-list-toolbar.vue +++ b/frontend/src/modules/member/components/list/member-list-toolbar.vue @@ -24,35 +24,6 @@ Merge contributors - - - - - {{ - enrichmentLabel - }} - - - Edit contributor - - - - -