@@ -13,12 +13,16 @@ import {
1313 Select ,
1414 Box ,
1515 Textarea ,
16+ Accordion ,
17+ ScrollArea ,
18+ Stack ,
1619} from "@mantine/core" ;
1720import { IconUserPlus , IconTrash } from "@tabler/icons-react" ;
1821import { notifications } from "@mantine/notifications" ;
1922import { illinoisNetId } from "@common/types/generic" ;
2023import { AuthGuard } from "@ui/components/AuthGuard" ;
2124import { AppRoles } from "@common/roles" ;
25+ import pluralize from "pluralize" ;
2226
2327interface ExternalMemberListManagementProps {
2428 fetchMembers : ( listId : string ) => Promise < string [ ] > ;
@@ -32,6 +36,7 @@ interface ExternalMemberListManagementProps {
3236}
3337
3438const ITEMS_PER_PAGE = 10 ;
39+ const CHANGE_DISPLAY_LIMIT = 10 ;
3540
3641const ExternalMemberListManagement : React . FC <
3742 ExternalMemberListManagementProps
@@ -202,25 +207,40 @@ const ExternalMemberListManagement: React.FC<
202207 } ;
203208
204209 const handleReplaceList = ( ) => {
205- const rawNetIds = replacementList
210+ const allLines = replacementList
206211 . split ( "\n" )
207- . map ( ( id ) => id . trim ( ) )
212+ . map ( ( line ) => line . trim ( ) )
208213 . filter ( Boolean ) ;
209214 const validNetIds = new Set < string > ( ) ;
210215 const invalidEntries : string [ ] = [ ] ;
211216
212- for ( const netId of rawNetIds ) {
213- if ( illinoisNetId . safeParse ( netId ) . success ) {
214- validNetIds . add ( netId ) ;
217+ for ( const line of allLines ) {
218+ // Rule: If it contains "@" but is not a valid "@illinois.edu" email, it's invalid.
219+ if ( line . includes ( "@" ) && ! line . endsWith ( "@illinois.edu" ) ) {
220+ invalidEntries . push ( line ) ;
221+ continue ;
222+ }
223+
224+ // Strip the domain to get the potential NetID for Zod validation
225+ const potentialNetId = line . replace ( "@illinois.edu" , "" ) ;
226+
227+ if ( illinoisNetId . safeParse ( potentialNetId ) . success ) {
228+ validNetIds . add ( potentialNetId ) ;
215229 } else {
216- invalidEntries . push ( netId ) ;
230+ invalidEntries . push ( line ) ; // Add the original failing line
217231 }
218232 }
219233
220234 if ( invalidEntries . length > 0 ) {
235+ const pluralize = ( singular : string , plural : string , count : number ) =>
236+ count === 1 ? singular : plural ;
237+ const verbIs = pluralize ( "is" , "are" , invalidEntries . length ) ;
238+ const verbHas = pluralize ( "has" , "have" , invalidEntries . length ) ;
239+ const entriesStr = invalidEntries . join ( ", " ) ;
240+
221241 notifications . show ( {
222242 title : "Invalid Entries Skipped" ,
223- message : `The following ${ invalidEntries . length } entries were invalid and have been ignored.` ,
243+ message : `${ entriesStr } ${ verbIs } invalid and ${ verbHas } been ignored.` ,
224244 color : "orange" ,
225245 } ) ;
226246 }
@@ -238,11 +258,19 @@ const ExternalMemberListManagement: React.FC<
238258
239259 setReplaceModalOpened ( false ) ;
240260 setReplacementList ( "" ) ;
241- notifications . show ( {
242- title : "Changes Computed" ,
243- message : `Queued ${ membersToAdd . length } additions and ${ membersToRemove . length } removals. Click 'Save Changes' to apply.` ,
244- color : "blue" ,
245- } ) ;
261+ if ( membersToAdd . length + membersToRemove . length > 0 ) {
262+ notifications . show ( {
263+ title : "Changes Computed" ,
264+ message : `Queued ${ membersToAdd . length } additions and ${ membersToRemove . length } removals. Click 'Save Changes' to apply.` ,
265+ color : "blue" ,
266+ } ) ;
267+ } else {
268+ notifications . show ( {
269+ title : "No Changes Found" ,
270+ message : `Both lists are the same.` ,
271+ color : "green" ,
272+ } ) ;
273+ }
246274 } ;
247275
248276 const rows = paginatedData . map ( ( member ) => {
@@ -357,7 +385,7 @@ const ExternalMemberListManagement: React.FC<
357385 < Table verticalSpacing = "sm" highlightOnHover >
358386 < Table . Thead >
359387 < Table . Tr >
360- < Table . Th > Member</ Table . Th >
388+ < Table . Th > Member NetID </ Table . Th >
361389 < Table . Th > Status</ Table . Th >
362390 < Table . Th > Actions</ Table . Th >
363391 </ Table . Tr >
@@ -398,16 +426,19 @@ const ExternalMemberListManagement: React.FC<
398426 </ Table . Tbody >
399427 </ Table >
400428
401- { totalPages > 1 && (
402- < Group justify = "center" mt = "md" >
429+ < Stack justify = "center" align = "center" mt = "md" >
430+ < Text size = "sm" c = "dimmed" >
431+ Found { members . length } { pluralize ( "member" , members . length ) } .
432+ </ Text >
433+ { totalPages > 1 && (
403434 < Pagination
404435 total = { totalPages }
405436 value = { activePage }
406437 onChange = { setPage }
407438 disabled = { isLoading }
408439 />
409- </ Group >
410- ) }
440+ ) }
441+ </ Stack >
411442
412443 < AuthGuard
413444 isAppShell = { false }
@@ -461,25 +492,65 @@ const ExternalMemberListManagement: React.FC<
461492 < Text fw = { 500 } size = "sm" mb = "xs" >
462493 Members to Add:
463494 </ Text >
464- { toAdd . map ( ( netId ) => (
465- < Text key = { netId } fz = "sm" >
466- { " " }
467- - { netId }
468- </ Text >
469- ) ) }
495+ { toAdd . length > CHANGE_DISPLAY_LIMIT ? (
496+ < Accordion variant = "separated" radius = "md" >
497+ < Accordion . Item value = "add-list" >
498+ < Accordion . Control >
499+ < Text fz = "sm" > { toAdd . length } members</ Text >
500+ </ Accordion . Control >
501+ < Accordion . Panel >
502+ < ScrollArea h = { 200 } >
503+ { toAdd . map ( ( netId ) => (
504+ < Text key = { netId } fz = "sm" py = { 2 } >
505+ - { netId }
506+ </ Text >
507+ ) ) }
508+ </ ScrollArea >
509+ </ Accordion . Panel >
510+ </ Accordion . Item >
511+ </ Accordion >
512+ ) : (
513+ toAdd . map ( ( netId ) => (
514+ < Text key = { netId } fz = "sm" >
515+ { " " }
516+ - { netId }
517+ </ Text >
518+ ) )
519+ ) }
470520 </ Box >
471521 ) }
472522 { toRemove . length > 0 && (
473523 < Box >
474524 < Text fw = { 500 } size = "sm" mb = "xs" >
475525 Members to Remove:
476526 </ Text >
477- { toRemove . map ( ( netId ) => (
478- < Text key = { netId } fz = "sm" c = "red" >
479- { " " }
480- - { netId }
481- </ Text >
482- ) ) }
527+ { toRemove . length > CHANGE_DISPLAY_LIMIT ? (
528+ < Accordion variant = "separated" radius = "md" >
529+ < Accordion . Item value = "remove-list" >
530+ < Accordion . Control >
531+ < Text fz = "sm" c = "red" >
532+ { toRemove . length } members
533+ </ Text >
534+ </ Accordion . Control >
535+ < Accordion . Panel >
536+ < ScrollArea h = { 200 } >
537+ { toRemove . map ( ( netId ) => (
538+ < Text key = { netId } fz = "sm" c = "red" py = { 2 } >
539+ - { netId }
540+ </ Text >
541+ ) ) }
542+ </ ScrollArea >
543+ </ Accordion . Panel >
544+ </ Accordion . Item >
545+ </ Accordion >
546+ ) : (
547+ toRemove . map ( ( netId ) => (
548+ < Text key = { netId } fz = "sm" c = "red" >
549+ { " " }
550+ - { netId }
551+ </ Text >
552+ ) )
553+ ) }
483554 </ Box >
484555 ) }
485556 < Group justify = "flex-end" mt = "lg" >
@@ -544,7 +615,7 @@ const ExternalMemberListManagement: React.FC<
544615 necessary additions and removals to match the list you provide.
545616 </ Text >
546617 < Textarea
547- placeholder = { "jdoe1\nasmith2\njohnson3 " }
618+ placeholder = { "jdoe2\[email protected] \njohns4 " } 548619 value = { replacementList }
549620 onChange = { ( e ) => setReplacementList ( e . currentTarget . value ) }
550621 autosize
0 commit comments