@@ -15,74 +15,122 @@ function memberOrganizationsOverlap<T extends IMemberOrganization>(a: T, b: T):
1515 )
1616}
1717
18- /**
19- * Finds email-domain rows that are represented by the same visible work experience.
20- */
21- export function getOverlappingEmailDomainMemberOrganizations < T extends IMemberOrganization > (
18+ function isCollapsibleMemberOrganization < T extends IMemberOrganization > ( row : T ) : boolean {
19+ if ( ! row . source ) {
20+ return false
21+ }
22+
23+ return row . source
24+ . split ( ',' )
25+ . map ( ( value ) => value . trim ( ) )
26+ . some ( ( value ) =>
27+ [ OrganizationSource . EMAIL_DOMAIN , OrganizationSource . PROJECT_REGISTRY ] . includes (
28+ value as OrganizationSource ,
29+ ) ,
30+ )
31+ }
32+
33+ function compareMemberOrganizationsBySourceRank (
34+ a : IMemberOrganization ,
35+ b : IMemberOrganization ,
36+ ) : number {
37+ const rankDiff =
38+ getMemberOrganizationSourceRank ( a . source ) - getMemberOrganizationSourceRank ( b . source )
39+ if ( rankDiff !== 0 ) {
40+ return rankDiff
41+ }
42+
43+ return ( a . id ?? '' ) . localeCompare ( b . id ?? '' )
44+ }
45+
46+ /** Hidden inferential rows that overlap a visible work experience (for delete/update/override). */
47+ export function getOverlappingGroupedMemberOrganizations < T extends IMemberOrganization > (
2248 rows : T [ ] ,
2349 memberOrganization : T ,
2450) : T [ ] {
2551 return rows . filter (
2652 ( row ) =>
2753 row . id !== memberOrganization . id &&
28- row . source === OrganizationSource . EMAIL_DOMAIN &&
54+ isCollapsibleMemberOrganization ( row ) &&
2955 memberOrganizationsOverlap ( row , memberOrganization ) ,
3056 )
3157}
3258
33- /**
34- * Groups overlapping email-domain rows into the best non-email-domain display row.
35- */
59+ function canDisplayCollapsibleRow < T extends IMemberOrganization > (
60+ displayRow : T ,
61+ collapsibleRow : T ,
62+ ) : boolean {
63+ if ( ! isCollapsibleMemberOrganization ( displayRow ) ) {
64+ return true
65+ }
66+
67+ return (
68+ getMemberOrganizationSourceRank ( displayRow . source ) <
69+ getMemberOrganizationSourceRank ( collapsibleRow . source )
70+ )
71+ }
72+
73+ /** Collapse overlapping email-domain and project-registry rows into one work experience for display. */
3674export function groupMemberOrganizations < T extends IMemberOrganization > ( rows : T [ ] ) : T [ ] {
37- const emailDomainRows = rows . filter ( ( row ) => row . source === OrganizationSource . EMAIL_DOMAIN )
38- const nonEmailRows = rows . filter ( ( row ) => row . source !== OrganizationSource . EMAIL_DOMAIN )
39- const hiddenEmailDomainIds = new Set < string > ( )
40- const displayGroups = new Map < string , { displayRow : T ; groupedEmailDomainRows : T [ ] } > ( )
41-
42- for ( const emailDomainRow of emailDomainRows . filter (
43- ( row ) : row is T & { id : string } => ! ! row . id ,
44- ) ) {
45- const overlappingNonEmailRows = nonEmailRows . filter ( ( row ) =>
46- memberOrganizationsOverlap ( emailDomainRow , row ) ,
75+ const collapsibleRows = rows . filter (
76+ ( row ) : row is T & { id : string } => ! ! row . id && isCollapsibleMemberOrganization ( row ) ,
77+ )
78+ const hiddenCollapsibleIds = new Set < string > ( )
79+ const collapsibleParentDisplayId = new Map < string , string > ( )
80+ const displayGroups = new Map < string , { displayRow : T & { id : string } ; groupedRows : T [ ] } > ( )
81+
82+ for ( const collapsibleRow of collapsibleRows ) {
83+ const overlappingDisplayRows = rows . filter (
84+ ( row ) : row is T & { id : string } =>
85+ ! ! row . id &&
86+ row . id !== collapsibleRow . id &&
87+ memberOrganizationsOverlap ( collapsibleRow , row ) &&
88+ canDisplayCollapsibleRow ( row , collapsibleRow ) ,
4789 )
4890
49- if ( overlappingNonEmailRows . length > 0 ) {
50- const displayRow = [ ...overlappingNonEmailRows ] . sort ( ( a , b ) => {
51- const rankDiff =
52- getMemberOrganizationSourceRank ( a . source ) - getMemberOrganizationSourceRank ( b . source )
53- if ( rankDiff !== 0 ) {
54- return rankDiff
55- }
91+ if ( overlappingDisplayRows . length > 0 ) {
92+ const displayRow = [ ...overlappingDisplayRows ] . sort ( compareMemberOrganizationsBySourceRank ) [ 0 ]
93+ hiddenCollapsibleIds . add ( collapsibleRow . id )
94+ collapsibleParentDisplayId . set ( collapsibleRow . id , displayRow . id )
95+ }
96+ }
5697
57- return ( a . id ?? '' ) . localeCompare ( b . id ?? '' )
58- } ) [ 0 ]
98+ const resolveDisplayRowId = ( collapsibleRowId : string ) : string => {
99+ let displayRowId = collapsibleRowId
100+ while ( collapsibleParentDisplayId . has ( displayRowId ) ) {
101+ displayRowId = collapsibleParentDisplayId . get ( displayRowId ) !
102+ }
103+ return displayRowId
104+ }
59105
60- if ( displayRow . id ) {
61- hiddenEmailDomainIds . add ( emailDomainRow . id )
106+ for ( const collapsibleRow of collapsibleRows ) {
107+ if ( hiddenCollapsibleIds . has ( collapsibleRow . id ) ) {
108+ const displayRowId = resolveDisplayRowId ( collapsibleRow . id )
109+ const displayRow = rows . find ( ( row ) : row is T & { id : string } => row . id === displayRowId )
62110
63- const existingGroup = displayGroups . get ( displayRow . id )
111+ if ( displayRow ) {
112+ const existingGroup = displayGroups . get ( displayRowId )
64113 if ( existingGroup ) {
65- existingGroup . groupedEmailDomainRows . push ( emailDomainRow )
114+ existingGroup . groupedRows . push ( collapsibleRow )
66115 } else {
67- displayGroups . set ( displayRow . id , {
116+ displayGroups . set ( displayRowId , {
68117 displayRow,
69- groupedEmailDomainRows : [ emailDomainRow ] ,
118+ groupedRows : [ collapsibleRow ] ,
70119 } )
71120 }
72121 }
73122 }
74123 }
75124
76125 return rows
77- . filter ( ( row ) : row is T & { id : string } => ! ! row . id && ! hiddenEmailDomainIds . has ( row . id ) )
126+ . filter ( ( row ) : row is T & { id : string } => ! ! row . id && ! hiddenCollapsibleIds . has ( row . id ) )
78127 . map ( ( row ) => {
79128 const group = displayGroups . get ( row . id )
80129 if ( ! group ) {
81130 return row
82131 }
83132
84- // Preserve the visible row while surfacing the combined sources and date range
85- const groupedRows = [ group . displayRow , ...group . groupedEmailDomainRows ]
133+ const groupedRows = [ group . displayRow , ...group . groupedRows ]
86134
87135 const normalizedStarts = groupedRows
88136 . map ( ( groupedRow ) => normalizeMemberOrganizationDate ( groupedRow . dateStart ) )
@@ -96,7 +144,12 @@ export function groupMemberOrganizations<T extends IMemberOrganization>(rows: T[
96144
97145 for ( const groupedRow of groupedRows ) {
98146 if ( groupedRow . source ) {
99- sources . add ( groupedRow . source )
147+ for ( const source of groupedRow . source . split ( ',' ) ) {
148+ const trimmed = source . trim ( )
149+ if ( trimmed ) {
150+ sources . add ( trimmed )
151+ }
152+ }
100153 }
101154 }
102155
0 commit comments