Skip to content

Commit 0adb4bf

Browse files
authored
refactor(profile): simplify work history & affiliations ux (#605)
* remove the Independent affiliations from project involvment section Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com> * update messaging Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com> * remove the work history review flow Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com> * update ambiguous phrasing under project involvement section Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com> * changed openSingleProjectTimeline from public to protected Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com> --------- Signed-off-by: Fayaz G <5818912+fayazg@users.noreply.github.com>
1 parent 395ba6a commit 0adb4bf

11 files changed

Lines changed: 122 additions & 469 deletions

File tree

apps/lfx-one/src/app/layouts/profile-layout/profile-layout.component.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { CombinedProfile, ProfileHeaderData, ProfileTab, ProfileUpdateRequest, U
1212
import { UserService } from '@services/user.service';
1313
import { MessageService } from 'primeng/api';
1414
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
15-
import { BehaviorSubject, catchError, combineLatest, filter, map, of, startWith, switchMap, take } from 'rxjs';
15+
import { BehaviorSubject, catchError, filter, map, of, startWith, switchMap, take } from 'rxjs';
1616

1717
import { stripAuthPrefixOrNull } from '@app/shared/utils/strip-auth-prefix.util';
1818
import { ProfileEditDialogComponent } from '../../modules/profile/components/profile-edit-dialog/profile-edit-dialog.component';
@@ -280,21 +280,12 @@ export class ProfileLayoutComponent {
280280

281281
private initTabNotifications(): Signal<Map<string, boolean>> {
282282
return toSignal(
283-
combineLatest([
284-
this.userService.getWorkExperiences().pipe(
285-
map((entries) => entries.some((e) => e.needsReview)),
286-
catchError(() => of(false)),
287-
startWith(false)
288-
),
289-
this.userService.getIdentities().pipe(
290-
map((identities) => identities.some((id) => id.platform !== 'lfid' && id.displayState !== 'hidden' && id.displayState !== 'verified')),
291-
catchError(() => of(false)),
292-
startWith(false)
293-
),
294-
]).pipe(
295-
map(([hasReviewable, hasUnverified]) => {
283+
this.userService.getIdentities().pipe(
284+
map((identities) => identities.some((id) => id.platform !== 'lfid' && id.displayState !== 'hidden' && id.displayState !== 'verified')),
285+
catchError(() => of(false)),
286+
startWith(false),
287+
map((hasUnverified) => {
296288
const notifications = new Map<string, boolean>();
297-
notifications.set('attribution', hasReviewable);
298289
notifications.set('identities', hasUnverified);
299290
return notifications;
300291
})

apps/lfx-one/src/app/modules/profile/affiliations/profile-affiliations.component.html

Lines changed: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -162,38 +162,45 @@ <h3 class="mb-1 text-base font-bold text-slate-900">Affiliations calculated from
162162
</div>
163163
</div>
164164

165-
<!-- Separator -->
166-
<div class="mt-3 border-t border-slate-200"></div>
167-
168165
<!-- Company rows -->
169-
@for (segment of group.segments; track segment.id; let lastSeg = $last) {
170-
<div
171-
class="flex items-center py-3"
172-
[class.border-b]="!lastSeg"
173-
[class.border-slate-100]="!lastSeg"
174-
[attr.data-testid]="'compact-segment-' + segment.id">
175-
<div class="flex items-center gap-2.5">
176-
<!-- Company logo -->
177-
@if (segment.organization === 'Independent') {
178-
<div class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-slate-200">
179-
<i class="fa-solid fa-user text-[10px] text-slate-500"></i>
180-
</div>
181-
} @else if (segment.organizationLogo) {
182-
<div class="flex h-6 w-6 shrink-0 items-center justify-center rounded border border-slate-100 bg-white">
183-
<img [src]="segment.organizationLogo" [alt]="segment.organization" class="h-4 w-4 object-contain" loading="lazy" />
184-
</div>
185-
} @else {
186-
<div class="flex h-6 w-6 shrink-0 items-center justify-center rounded border border-slate-100 bg-white">
187-
<i class="fa-light fa-building text-[10px] text-slate-400"></i>
188-
</div>
189-
}
166+
@if (group.segments.length === 0) {
167+
<div class="flex items-center pt-3" [attr.data-testid]="'compact-empty-' + group.id">
168+
<button
169+
class="flex cursor-pointer items-center gap-2 text-sm text-slate-400 transition-colors hover:text-blue-600"
170+
(click)="openSingleProjectTimeline(group)"
171+
[attr.data-testid]="'compact-add-affiliation-' + group.id">
172+
<i class="fa-light fa-plus-circle text-base"></i>
173+
<span>No project specific affiliation set — add one</span>
174+
</button>
175+
</div>
176+
} @else {
177+
<!-- Separator -->
178+
<div class="mt-3 border-t border-slate-200"></div>
179+
@for (segment of group.segments; track segment.id; let lastSeg = $last) {
180+
<div
181+
class="flex items-center py-3"
182+
[class.border-b]="!lastSeg"
183+
[class.border-slate-100]="!lastSeg"
184+
[attr.data-testid]="'compact-segment-' + segment.id">
185+
<div class="flex items-center gap-2.5">
186+
<!-- Company logo -->
187+
@if (segment.organizationLogo) {
188+
<div class="flex h-6 w-6 shrink-0 items-center justify-center rounded border border-slate-100 bg-white">
189+
<img [src]="segment.organizationLogo" [alt]="segment.organization" class="h-4 w-4 object-contain" loading="lazy" />
190+
</div>
191+
} @else {
192+
<div class="flex h-6 w-6 shrink-0 items-center justify-center rounded border border-slate-100 bg-white">
193+
<i class="fa-light fa-building text-[10px] text-slate-400"></i>
194+
</div>
195+
}
190196

191-
<div class="flex flex-col">
192-
<span class="text-sm font-bold text-slate-900">{{ segment.organization }}</span>
193-
<span class="text-sm text-slate-500">{{ segment.startDate }} – {{ segment.endDate || 'Present' }}</span>
197+
<div class="flex flex-col">
198+
<span class="text-sm font-bold text-slate-900">{{ segment.organization }}</span>
199+
<span class="text-sm text-slate-500">{{ segment.startDate }} – {{ segment.endDate || 'Present' }}</span>
200+
</div>
194201
</div>
195202
</div>
196-
</div>
203+
}
197204
}
198205
</div>
199206
}
@@ -332,28 +339,34 @@ <h3 class="mb-1 text-base font-bold text-slate-900">Affiliations calculated from
332339
<!-- Affiliation Column -->
333340
<td class="px-5 py-3.5">
334341
<div class="flex items-center justify-between">
335-
<div class="flex items-center gap-2.5">
336-
<!-- Org Logo -->
337-
@if (row.segment.organization === 'Independent') {
338-
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-slate-200">
339-
<i class="fa-solid fa-user text-xs text-slate-500"></i>
340-
</div>
341-
} @else if (row.segment.organizationLogo) {
342-
<div class="flex h-7 w-7 items-center justify-center rounded-md border border-slate-100 bg-white shadow-sm">
343-
<img [src]="row.segment.organizationLogo" [alt]="row.segment.organization" class="h-4 w-4 object-contain" loading="lazy" />
344-
</div>
345-
} @else {
346-
<div class="flex h-7 w-7 items-center justify-center rounded-md border border-slate-100 bg-white shadow-sm">
347-
<i class="fa-light fa-building text-xs text-slate-400"></i>
348-
</div>
349-
}
342+
@if (row.isPlaceholder) {
343+
<button
344+
class="flex cursor-pointer items-center gap-2 text-sm text-slate-400 transition-colors hover:text-blue-600"
345+
(click)="openSingleProjectTimeline(row.group)"
346+
[attr.data-testid]="'add-affiliation-' + row.group.id">
347+
<i class="fa-light fa-plus-circle text-base"></i>
348+
<span>No project specific affiliation set — add one</span>
349+
</button>
350+
} @else {
351+
<div class="flex items-center gap-2.5">
352+
<!-- Org Logo -->
353+
@if (row.segment.organizationLogo) {
354+
<div class="flex h-7 w-7 items-center justify-center rounded-md border border-slate-100 bg-white shadow-sm">
355+
<img [src]="row.segment.organizationLogo" [alt]="row.segment.organization" class="h-4 w-4 object-contain" loading="lazy" />
356+
</div>
357+
} @else {
358+
<div class="flex h-7 w-7 items-center justify-center rounded-md border border-slate-100 bg-white shadow-sm">
359+
<i class="fa-light fa-building text-xs text-slate-400"></i>
360+
</div>
361+
}
350362

351-
<!-- Org Name & Dates -->
352-
<div class="flex flex-col">
353-
<span class="text-sm font-medium text-slate-700">{{ row.segment.organization }}</span>
354-
<span class="text-[10px] text-slate-400">{{ row.segment.startDate }} – {{ row.segment.endDate || 'Present' }}</span>
363+
<!-- Org Name & Dates -->
364+
<div class="flex flex-col">
365+
<span class="text-sm font-medium text-slate-700">{{ row.segment.organization }}</span>
366+
<span class="text-[10px] text-slate-400">{{ row.segment.startDate }} – {{ row.segment.endDate || 'Present' }}</span>
367+
</div>
355368
</div>
356-
</div>
369+
}
357370

358371
<!-- 3-dot menu -->
359372
<div class="relative">

apps/lfx-one/src/app/modules/profile/affiliations/profile-affiliations.component.ts

Lines changed: 38 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export class ProfileAffiliationsComponent {
173173
void this.router.navigate(['/profile', 'identities']);
174174
}
175175

176-
private openSingleProjectTimeline(project: ProjectGroup): void {
176+
protected openSingleProjectTimeline(project: ProjectGroup): void {
177177
const timelineProjects: TimelineProjectData[] = [
178178
{
179179
projectName: project.projectName,
@@ -208,27 +208,7 @@ export class ProfileAffiliationsComponent {
208208
const idx = groups.findIndex((g) => g.projectName === updated.projectName);
209209
if (idx === -1) continue;
210210

211-
if (updated.segments.length > 0) {
212-
groups[idx] = { ...groups[idx], segments: updated.segments };
213-
} else {
214-
const originalSegments = groups[idx].segments;
215-
const earliest = originalSegments.reduce((min, seg) => (seg.startDate < min ? seg.startDate : min), originalSegments[0]?.startDate || 'Jan 2020');
216-
groups[idx] = {
217-
...groups[idx],
218-
segments: [
219-
{
220-
id: `${groups[idx].id}-independent`,
221-
role: 'Contributor' as const,
222-
roleSource: 'user-overridden' as const,
223-
organization: 'Independent',
224-
startDate: earliest,
225-
sourceLabel: 'Confirmed by user' as const,
226-
sourceType: 'user-overridden' as const,
227-
needsConfirmation: false,
228-
},
229-
],
230-
};
231-
}
211+
groups[idx] = { ...groups[idx], segments: updated.segments };
232212
}
233213
this.projectGroups.set(groups);
234214
}
@@ -261,9 +241,7 @@ export class ProfileAffiliationsComponent {
261241
const cdpProject = cdpAffiliations.find((p) => p.projectName === updated.projectName);
262242
if (!cdpProject) continue;
263243

264-
const affiliations: CdpProjectAffiliationEntry[] = updated.segments
265-
.filter((seg) => seg.organization !== 'Independent')
266-
.map((seg) => {
244+
const affiliations: CdpProjectAffiliationEntry[] = updated.segments.map((seg) => {
267245
const orgMeta = orgLookup.get(seg.organization);
268246
const isTemporaryId = !seg.id || seg.id.startsWith('period-') || seg.id.endsWith('-independent');
269247

@@ -341,14 +319,25 @@ export class ProfileAffiliationsComponent {
341319
for (let g = 0; g < groups.length; g++) {
342320
const group = groups[g];
343321
const isLastGroup = g === groups.length - 1;
344-
for (let i = 0; i < group.segments.length; i++) {
322+
if (group.segments.length === 0) {
345323
rows.push({
346324
group,
347-
segment: group.segments[i],
348-
isFirstSegment: i === 0,
349-
isLastSegmentInGroup: i === group.segments.length - 1,
325+
segment: { id: `${group.id}-empty`, role: 'Contributor', roleSource: 'cdp-detected', organization: '', startDate: '', sourceLabel: 'Derived from work experience', sourceType: 'inferred', needsConfirmation: false },
326+
isFirstSegment: true,
327+
isLastSegmentInGroup: true,
350328
isLastGroup,
329+
isPlaceholder: true,
351330
});
331+
} else {
332+
for (let i = 0; i < group.segments.length; i++) {
333+
rows.push({
334+
group,
335+
segment: group.segments[i],
336+
isFirstSegment: i === 0,
337+
isLastSegmentInGroup: i === group.segments.length - 1,
338+
isLastGroup,
339+
});
340+
}
352341
}
353342
}
354343
return rows;
@@ -390,14 +379,13 @@ export class ProfileAffiliationsComponent {
390379
return computed(() => {
391380
const map = new Map<string, MenuItem[]>();
392381
for (const project of this.sortedProjectGroups()) {
393-
for (const segment of project.segments) {
394-
map.set(segment.id, [
395-
{
396-
label: 'Edit Affiliation',
397-
icon: 'fa-light fa-pencil',
398-
command: () => this.openSingleProjectTimeline(project),
399-
},
400-
]);
382+
const editItem = [{ label: 'Edit Affiliation', icon: 'fa-light fa-pencil', command: () => this.openSingleProjectTimeline(project) }];
383+
if (project.segments.length === 0) {
384+
map.set(`${project.id}-empty`, editItem);
385+
} else {
386+
for (const segment of project.segments) {
387+
map.set(segment.id, editItem);
388+
}
401389
}
402390
}
403391
return map;
@@ -433,7 +421,7 @@ export class ProfileAffiliationsComponent {
433421

434422
const verifiedOrgIds = new Set(
435423
this.workExperience()
436-
.filter((we) => !we.needsReview && we.organizationId)
424+
.filter((we) => we.organizationId)
437425
.map((we) => we.organizationId!)
438426
);
439427

@@ -448,42 +436,18 @@ export class ProfileAffiliationsComponent {
448436
(aff) => aff.type === 'project' && (aff.verifiedBy === lfid || verifiedOrgIds.has(aff.organizationId))
449437
);
450438

451-
const segments: AffiliationSegment[] =
452-
projectAffiliations.length > 0
453-
? projectAffiliations.map((aff) => {
454-
return {
455-
id: aff.id ?? '',
456-
role,
457-
roleSource,
458-
organization: aff.organizationName,
459-
organizationLogo: aff.organizationLogo || undefined,
460-
startDate: isoDateToMonthYear(aff.startDate),
461-
endDate: aff.endDate ? isoDateToMonthYear(aff.endDate) : undefined,
462-
sourceLabel: aff.verified ? ('Confirmed by user' as const) : ('Derived from work experience' as const),
463-
sourceType: aff.verified ? ('user-confirmed' as const) : ('inferred' as const),
464-
needsConfirmation: !aff.verified && role === 'Maintainer' && roleSource === 'cdp-detected',
465-
};
466-
})
467-
: (() => {
468-
const allStartDates = project.affiliations.map((a) => a.startDate).filter(Boolean);
469-
const allEndDates = project.affiliations.map((a) => a.endDate).filter((d): d is string => !!d);
470-
const earliestStart = allStartDates.length > 0 ? allStartDates.sort()[0] : undefined;
471-
const latestEnd = allEndDates.length > 0 ? allEndDates.sort().reverse()[0] : undefined;
472-
473-
return [
474-
{
475-
id: `${project.id}-independent`,
476-
role,
477-
roleSource,
478-
organization: 'Independent',
479-
startDate: earliestStart ? isoDateToMonthYear(earliestStart) : 'Unknown',
480-
endDate: latestEnd ? isoDateToMonthYear(latestEnd) : undefined,
481-
sourceLabel: 'Derived from work experience' as const,
482-
sourceType: 'inferred' as const,
483-
needsConfirmation: false,
484-
},
485-
];
486-
})();
439+
const segments: AffiliationSegment[] = projectAffiliations.map((aff) => ({
440+
id: aff.id ?? '',
441+
role,
442+
roleSource,
443+
organization: aff.organizationName,
444+
organizationLogo: aff.organizationLogo || undefined,
445+
startDate: isoDateToMonthYear(aff.startDate),
446+
endDate: aff.endDate ? isoDateToMonthYear(aff.endDate) : undefined,
447+
sourceLabel: aff.verified ? ('Confirmed by user' as const) : ('Derived from work experience' as const),
448+
sourceType: aff.verified ? ('user-confirmed' as const) : ('inferred' as const),
449+
needsConfirmation: !aff.verified && role === 'Maintainer' && roleSource === 'cdp-detected',
450+
}));
487451

488452
const enabledOrgIds = new Set(projectAffiliations.map((aff) => aff.organizationId));
489453
const disabledOrgSuggestions = this.buildDisabledOrgSuggestions(project, enabledOrgIds, lfid);

apps/lfx-one/src/app/modules/profile/attribution/profile-attribution.component.html

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,12 @@ <h2>Work history</h2>
1818
data-testid="add-work-experience-header-btn" />
1919
}
2020
</div>
21-
<p class="mb-4 text-[13px] leading-normal text-slate-500">
22-
Work experiences determine which organization receives credit for your contributions during each time period.
23-
</p>
2421
<lfx-profile-work-experience [hideHeader]="true" (workExperienceChanged)="onWorkExperienceChanged()" />
2522
</div>
2623

2724
<!-- Right Column: Project Involvement -->
2825
<div class="flex w-[360px] flex-col gap-3">
2926
<h2>Project Involvement</h2>
30-
<p class="text-[13px] leading-normal text-slate-500">
31-
Your contributions are automatically attributed based on your verified identities and work history.
32-
</p>
3327
<lfx-profile-affiliations [hideHeader]="true" [compact]="true" (addWorkExperience)="onAddWorkExperience()" />
3428
</div>
3529
</div>

apps/lfx-one/src/app/modules/profile/components/affiliation-timeline-dialog/affiliation-timeline-dialog.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ export class AffiliationTimelineDialogComponent {
177177
const orgMap = new Map<string, AffiliationEditOrg>();
178178

179179
for (const we of weEntries) {
180-
if (we.needsReview) continue;
181180
orgMap.set(we.organization, {
182181
organization: we.organization,
183182
organizationLogo: we.organizationLogo,

0 commit comments

Comments
 (0)