Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(openchallenges): show role of organizations in Contributors section #2284

Merged
merged 9 commits into from
Nov 3, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,48 @@ <h3>Contributors</h3>
from planning and organizing the challenge to contributing data and providing technical
support.
</p>
<ng-container *ngIf="organizationCards!.length > 0; else na_text">
<div class="card-group">
<div id="contributor-organizer">
<p class="mat-body-strong"><strong>Challenge Organizers</strong></p>
<ng-container *ngIf="organizerCardBundles.length > 0; else no_org">
<div class="card-group">
<openchallenges-organization-card
*ngFor="let bundle of organizerCardBundles"
[organizationCard]="bundle.card"
[showMember]="false"
/>
</div>
</ng-container>
<ng-template #no_org>
<p class="text-grey">Not available</p>
</ng-template>
</div>
<div id="contributor-data">
<p class="mat-body-strong"><strong>Data Contributors</strong></p>
<ng-container *ngIf="dataContributorCardBundles.length > 0; else no_data">
<div class="card-group">
<openchallenges-organization-card
*ngFor="let bundle of dataContributorCardBundles"
[organizationCard]="bundle.card"
[showMember]="false"
/>
</div>
</ng-container>
<ng-template #no_data>
<p class="text-grey">Not available</p>
</ng-template>
</div>
<div id="contributor-sponsor">
<p class="mat-body-strong"><strong>Sponsors</strong></p>
<ng-container *ngIf="sponsorCardBundles.length > 0; else no_sponsor">
<openchallenges-organization-card
*ngFor="let organizationCard of organizationCards"
[organizationCard]="organizationCard"
*ngFor="let bundle of sponsorCardBundles"
[organizationCard]="bundle.card"
[showMember]="false"
/>
</div>
</ng-container>
<ng-template #na_text>
<h4 class="text-grey">Not available</h4>
</ng-template>
</ng-container>
<ng-template #no_sponsor>
<p class="text-grey">Not available</p>
</ng-template>
</div>
</div>
</main>
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#contributors h3 {
margin-top: 14px;
}

.text-grey {
margin: 2em 0;
[id^=contributor-] {
margin-bottom: 32px;
}
.card-group {
padding: 3px 0;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import {
Observable,
catchError,
forkJoin,
iif,
map,
of,
shareReplay,
switchMap,
take,
} from 'rxjs';
import {
Challenge,
ChallengeContribution,
ChallengeContributionRole,
ChallengeContributionService,
Image,
ImageAspectRatio,
Expand All @@ -17,15 +29,12 @@ import {
OrganizationCardComponent,
} from '@sagebionetworks/openchallenges/ui';
import { forkJoinConcurrent } from '@sagebionetworks/openchallenges/util';
import {
Observable,
catchError,
forkJoin,
iif,
map,
of,
switchMap,
} from 'rxjs';
import { orderBy } from 'lodash';

type ContributionCardBundle = {
card: OrganizationCard;
role: ChallengeContributionRole;
};

@Component({
selector: 'openchallenges-challenge-contributors',
Expand All @@ -36,88 +45,136 @@ import {
})
export class ChallengeContributorsComponent implements OnInit {
@Input({ required: true }) challenge!: Challenge;
organizationCards: OrganizationCard[] = [];

organizerCardBundles: ContributionCardBundle[] = [];
dataContributorCardBundles: ContributionCardBundle[] = [];
sponsorCardBundles: ContributionCardBundle[] = [];

constructor(
private challengeContributionService: ChallengeContributionService,
private organizationService: OrganizationService,
private imageService: ImageService
) {}

ngOnInit() {
this.challengeContributionService
.listChallengeContributions(this.challenge.id)
.pipe(
switchMap((page) =>
forkJoinConcurrent(
this.uniqueContributions(page.challengeContributions).map(
(contribution) =>
this.organizationService.getOrganization(
contribution.organizationId.toString()
)
),
Infinity
)
),
map((orgs: Organization[]) => this.sortOrgs(orgs)),
switchMap((orgs) =>
forkJoin({
orgs: of(orgs),
avatarUrls: forkJoinConcurrent(
orgs.map((org) => this.getOrganizationAvatarUrl(org)),
Infinity
),
})
),
switchMap(({ orgs, avatarUrls }) =>
of(
orgs.map((org, index) =>
this.getOrganizationCard(org, avatarUrls[index])
)
)
// Get all the contributions
const contribs$: Observable<ChallengeContribution[]> =
this.challengeContributionService
.listChallengeContributions(this.challenge.id)
.pipe(
map((page) => page.challengeContributions),
shareReplay(1),
take(1)
);

// Get a list of unique orgs to minimize outbound requests
const orgs$: Observable<Organization[]> = contribs$.pipe(
map((contribs) => [...new Set(contribs.map((c) => c.organizationId))]),
switchMap((orgIds) =>
forkJoinConcurrent(
orgIds.map((orgId) =>
this.organizationService.getOrganization(orgId.toString())
),
Infinity
)
),
shareReplay(1),
take(1)
);

// Get the logo of the orgs
const orgLogos$ = orgs$.pipe(
switchMap((orgs) =>
forkJoinConcurrent(
orgs.map((org) => this.getOrganizationAvatarUrl(org)),
Infinity
)
)
.subscribe((orgCards) => (this.organizationCards = orgCards));
}
);

private uniqueContributions(
contributions: ChallengeContribution[]
): ChallengeContribution[] {
return contributions.filter(
(b, i) =>
contributions.findIndex(
(a) => a.organizationId === b.organizationId
) === i
// Creates the contribution card bundles
const contributionCardBundles$ = forkJoin({
contribs: contribs$,
orgs: orgs$,
orgLogos: orgLogos$,
}).pipe(
map((data) => {
const contributionCardBundles: ContributionCardBundle[] = [];
for (const contrib of data.contribs) {
const orgIndex = data.orgs
.map((org) => org.id)
.indexOf(contrib.organizationId);
const org = orgIndex >= 0 ? data.orgs[orgIndex] : undefined;
const orgLogo = orgIndex >= 0 ? data.orgLogos[orgIndex] : undefined;
if (org !== undefined) {
contributionCardBundles.push({
card: this.getOrganizationCard(org, orgLogo),
role: contrib.role,
});
}
}
return contributionCardBundles;
}),
shareReplay(1),
take(1)
);

// Get the organizer card bundles
this.getContributionCardBundlesByRole(
contributionCardBundles$,
ChallengeContributionRole.ChallengeOrganizer
).subscribe((bundles) => (this.organizerCardBundles = bundles));

// Get the data contributor card bundles
this.getContributionCardBundlesByRole(
contributionCardBundles$,
ChallengeContributionRole.DataContributor
).subscribe((bundles) => (this.dataContributorCardBundles = bundles));

// Get the sponsor card bundles
this.getContributionCardBundlesByRole(
contributionCardBundles$,
ChallengeContributionRole.Sponsor
).subscribe((bundles) => (this.sponsorCardBundles = bundles));
}

private sortOrgs(orgs: Organization[]): Organization[] {
return orgs.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
private getContributionCardBundlesByRole(
bundles$: Observable<ContributionCardBundle[]>,
role: ChallengeContributionRole
): Observable<ContributionCardBundle[]> {
return bundles$.pipe(
map((bundles) => bundles.filter((b) => b.role === role)),
// order orgs by the number of challenges they have contributed to
map((bundles) => orderBy(bundles, ['card.challengeCount'], ['desc']))
);
}

// TODO Avoid duplicated code (see org search component)
private getOrganizationAvatarUrl(org: Organization): Observable<Image> {
private getOrganizationAvatarUrl(
org: Organization
): Observable<Image | undefined> {
return iif(
() => !!org.avatarKey,
this.imageService.getImage({
objectKey: org.avatarKey,
height: ImageHeight._140px,
aspectRatio: ImageAspectRatio._11,
} as ImageQuery),
of({ url: '' })
of(undefined)
).pipe(
catchError(() => {
console.error(
'Unable to get the image url. Please check the logs of the image service.'
);
return of({ url: '' });
return of(undefined);
})
);
}

// TODO Avoid duplicated code (see org search component)
private getOrganizationCard(
org: Organization,
avatarUrl: Image
avatarUrl: Image | undefined
): OrganizationCard {
return {
acronym: org.acronym,
Expand Down