@@ -15,74 +15,125 @@ 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+ [
28+ OrganizationSource . EMAIL_DOMAIN ,
29+ OrganizationSource . PROJECT_REGISTRY ,
30+ ] . includes ( value as OrganizationSource ) ,
31+ )
32+ }
33+
34+ function compareMemberOrganizationsBySourceRank (
35+ a : IMemberOrganization ,
36+ b : IMemberOrganization ,
37+ ) : number {
38+ const rankDiff =
39+ getMemberOrganizationSourceRank ( a . source ) - getMemberOrganizationSourceRank ( b . source )
40+ if ( rankDiff !== 0 ) {
41+ return rankDiff
42+ }
43+
44+ return ( a . id ?? '' ) . localeCompare ( b . id ?? '' )
45+ }
46+
47+ /** Hidden inferential rows that overlap a visible work experience (for delete/update/override). */
48+ export function getOverlappingGroupedMemberOrganizations < T extends IMemberOrganization > (
2249 rows : T [ ] ,
2350 memberOrganization : T ,
2451) : T [ ] {
2552 return rows . filter (
2653 ( row ) =>
2754 row . id !== memberOrganization . id &&
28- row . source === OrganizationSource . EMAIL_DOMAIN &&
55+ isCollapsibleMemberOrganization ( row ) &&
2956 memberOrganizationsOverlap ( row , memberOrganization ) ,
3057 )
3158}
3259
33- /**
34- * Groups overlapping email-domain rows into the best non-email-domain display row.
35- */
60+ function canDisplayCollapsibleRow < T extends IMemberOrganization > (
61+ displayRow : T ,
62+ collapsibleRow : T ,
63+ ) : boolean {
64+ if ( ! isCollapsibleMemberOrganization ( displayRow ) ) {
65+ return true
66+ }
67+
68+ return (
69+ getMemberOrganizationSourceRank ( displayRow . source ) <
70+ getMemberOrganizationSourceRank ( collapsibleRow . source )
71+ )
72+ }
73+
74+ /** Collapse overlapping email-domain and project-registry rows into one work experience for display. */
3675export 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 ) ,
76+ const collapsibleRows = rows . filter (
77+ ( row ) : row is T & { id : string } => ! ! row . id && isCollapsibleMemberOrganization ( row ) ,
78+ )
79+ const hiddenCollapsibleIds = new Set < string > ( )
80+ const collapsibleParentDisplayId = new Map < string , string > ( )
81+ const displayGroups = new Map < string , { displayRow : T & { id : string } ; groupedRows : T [ ] } > ( )
82+
83+ for ( const collapsibleRow of collapsibleRows ) {
84+ const overlappingDisplayRows = rows . filter (
85+ ( row ) : row is T & { id : string } =>
86+ ! ! row . id &&
87+ row . id !== collapsibleRow . id &&
88+ memberOrganizationsOverlap ( collapsibleRow , row ) &&
89+ canDisplayCollapsibleRow ( row , collapsibleRow ) ,
4790 )
4891
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- }
92+ if ( overlappingDisplayRows . length > 0 ) {
93+ const displayRow = [ ...overlappingDisplayRows ] . sort ( compareMemberOrganizationsBySourceRank ) [ 0 ]
94+ hiddenCollapsibleIds . add ( collapsibleRow . id )
95+ collapsibleParentDisplayId . set ( collapsibleRow . id , displayRow . id )
96+ }
97+ }
5698
57- return ( a . id ?? '' ) . localeCompare ( b . id ?? '' )
58- } ) [ 0 ]
99+ const resolveDisplayRowId = ( collapsibleRowId : string ) : string => {
100+ let displayRowId = collapsibleRowId
101+ while ( collapsibleParentDisplayId . has ( displayRowId ) ) {
102+ displayRowId = collapsibleParentDisplayId . get ( displayRowId ) !
103+ }
104+ return displayRowId
105+ }
59106
60- if ( displayRow . id ) {
61- hiddenEmailDomainIds . add ( emailDomainRow . id )
107+ for ( const collapsibleRow of collapsibleRows ) {
108+ if ( hiddenCollapsibleIds . has ( collapsibleRow . id ) ) {
109+ const displayRowId = resolveDisplayRowId ( collapsibleRow . id )
110+ const displayRow = rows . find (
111+ ( row ) : row is T & { id : string } => row . id === displayRowId ,
112+ )
62113
63- const existingGroup = displayGroups . get ( displayRow . id )
114+ if ( displayRow ) {
115+ const existingGroup = displayGroups . get ( displayRowId )
64116 if ( existingGroup ) {
65- existingGroup . groupedEmailDomainRows . push ( emailDomainRow )
117+ existingGroup . groupedRows . push ( collapsibleRow )
66118 } else {
67- displayGroups . set ( displayRow . id , {
119+ displayGroups . set ( displayRowId , {
68120 displayRow,
69- groupedEmailDomainRows : [ emailDomainRow ] ,
121+ groupedRows : [ collapsibleRow ] ,
70122 } )
71123 }
72124 }
73125 }
74126 }
75127
76128 return rows
77- . filter ( ( row ) : row is T & { id : string } => ! ! row . id && ! hiddenEmailDomainIds . has ( row . id ) )
129+ . filter ( ( row ) : row is T & { id : string } => ! ! row . id && ! hiddenCollapsibleIds . has ( row . id ) )
78130 . map ( ( row ) => {
79131 const group = displayGroups . get ( row . id )
80132 if ( ! group ) {
81133 return row
82134 }
83135
84- // Preserve the visible row while surfacing the combined sources and date range
85- const groupedRows = [ group . displayRow , ...group . groupedEmailDomainRows ]
136+ const groupedRows = [ group . displayRow , ...group . groupedRows ]
86137
87138 const normalizedStarts = groupedRows
88139 . map ( ( groupedRow ) => normalizeMemberOrganizationDate ( groupedRow . dateStart ) )
@@ -96,7 +147,12 @@ export function groupMemberOrganizations<T extends IMemberOrganization>(rows: T[
96147
97148 for ( const groupedRow of groupedRows ) {
98149 if ( groupedRow . source ) {
99- sources . add ( groupedRow . source )
150+ for ( const source of groupedRow . source . split ( ',' ) ) {
151+ const trimmed = source . trim ( )
152+ if ( trimmed ) {
153+ sources . add ( trimmed )
154+ }
155+ }
100156 }
101157 }
102158
0 commit comments