99 AlertDialog ,
1010 Tooltip ,
1111 useThemeContext ,
12+ Callout ,
1213} from "@radix-ui/themes" ;
1314import {
1415 Await ,
@@ -85,10 +86,10 @@ function CollaboratorsTable({
8586
8687 const { appearance } = useThemeContext ( ) ;
8788
88- // Determine if the table is editable (current user is admin)
89- const isEditable = useMemo ( ( ) => {
89+ // Check if current user is an admin
90+ const isCurrentUserAdmin = useMemo ( ( ) => {
91+ if ( ! currentUser ) return false ;
9092 return (
91- currentUser &&
9293 collaborators . find ( ( c : Collaborator ) => c . userId === currentUser . id )
9394 ?. role === "admin"
9495 ) ;
@@ -129,14 +130,13 @@ function CollaboratorsTable({
129130
130131 useEffect ( ( ) => {
131132 // Only update draft state when safe to do so:
132- // - If table is not editable (user can't make changes anyway)
133- // - If table is editable but user has no pending changes
134- // Note: When isEditable && hasEdited, we preserve user's draft changes
135- if ( ! isEditable || ! hasEdited ) {
133+ // - Sync with server data when there are no pending edits
134+ // - Preserve draft when there are pending edits (admin only can edit)
135+ if ( ! hasEdited ) {
136136 collaboratorsRef . current = collaborators ;
137137 setDraftCollaborators ( collaborators ) ;
138138 }
139- } , [ collaborators , isEditable , hasEdited ] ) ;
139+ } , [ collaborators , hasEdited ] ) ;
140140
141141 const changeRole = useCallback (
142142 ( collaborator : Collaborator , newRole : "admin" | "collaborator" ) => {
@@ -257,6 +257,17 @@ function CollaboratorsTable({
257257
258258 return (
259259 < >
260+ { ! isCurrentUserAdmin && (
261+ < Callout . Root size = "1" variant = "soft" className = "py-2!" mt = "5" >
262+ < Callout . Icon >
263+ < InfoCircledIcon />
264+ </ Callout . Icon >
265+ < Callout . Text >
266+ You will need admin privileges to update this setting.
267+ </ Callout . Text >
268+ </ Callout . Root >
269+ ) }
270+
260271 < Table . Root mt = "5" >
261272 < Table . Header >
262273 < Table . Row >
@@ -286,9 +297,7 @@ function CollaboratorsTable({
286297 </ Flex >
287298 </ Tooltip >
288299 </ Table . ColumnHeaderCell >
289- { isEditable && (
290- < Table . ColumnHeaderCell width = "auto" > </ Table . ColumnHeaderCell >
291- ) }
300+ < Table . ColumnHeaderCell width = "auto" > </ Table . ColumnHeaderCell >
292301 </ Table . Row >
293302 </ Table . Header >
294303
@@ -318,8 +327,6 @@ function CollaboratorsTable({
318327 draftCollaborators . filter ( ( c ) => c . role === "admin" ) . length ===
319328 1 ;
320329
321- // Get current user's role on this board (for reference, actual editability is determined by isEditable)
322-
323330 return (
324331 < Table . Row key = { collaborator . userId } >
325332 < Table . RowHeaderCell
@@ -346,147 +353,140 @@ function CollaboratorsTable({
346353 >
347354 { role }
348355 </ Table . Cell >
349- { isEditable && (
350- < Table . Cell className = "align-middle!" >
351- < DropdownMenu . Root >
352- < DropdownMenu . Trigger >
353- < Button variant = "soft" highContrast >
354- Edit
355- < DropdownMenu . TriggerIcon />
356- </ Button >
357- </ DropdownMenu . Trigger >
358- < DropdownMenu . Content >
359- < DropdownMenu . Group >
360- < DropdownMenu . Label >
361- Select roles
362- </ DropdownMenu . Label >
363- < DropdownMenu . CheckboxItem
364- checked = { role === "admin" && ! hasRemoved }
365- onCheckedChange = { ( ) =>
366- changeRole ( collaborator , "admin" )
367- }
368- >
369- Admin
370- </ DropdownMenu . CheckboxItem >
371- < DropdownMenu . CheckboxItem
372- checked = { role === "collaborator" && ! hasRemoved }
373- onCheckedChange = { ( ) =>
374- changeRole ( collaborator , "collaborator" )
375- }
376- disabled = { isOnlyActiveAdmin }
377- >
378- Collaborator
379- </ DropdownMenu . CheckboxItem >
380- </ DropdownMenu . Group >
381- < DropdownMenu . Separator />
382-
383- < DropdownMenu . Item
384- onSelect = { ( ) => {
385- removeCollaborator ( collaborator ) ;
386- } }
387- disabled = { isCurrentUser || isOnlyActiveAdmin }
356+ < Table . Cell className = "align-middle!" >
357+ < DropdownMenu . Root >
358+ < DropdownMenu . Trigger disabled = { ! isCurrentUserAdmin } >
359+ < Button variant = "soft" highContrast >
360+ Edit
361+ < DropdownMenu . TriggerIcon />
362+ </ Button >
363+ </ DropdownMenu . Trigger >
364+ < DropdownMenu . Content >
365+ < DropdownMenu . Group >
366+ < DropdownMenu . Label > Select roles</ DropdownMenu . Label >
367+ < DropdownMenu . CheckboxItem
368+ checked = { role === "admin" && ! hasRemoved }
369+ onCheckedChange = { ( ) =>
370+ changeRole ( collaborator , "admin" )
371+ }
388372 >
389- Remove access
390- </ DropdownMenu . Item >
391- </ DropdownMenu . Content >
392- </ DropdownMenu . Root >
393- </ Table . Cell >
394- ) }
373+ Admin
374+ </ DropdownMenu . CheckboxItem >
375+ < DropdownMenu . CheckboxItem
376+ checked = { role === "collaborator" && ! hasRemoved }
377+ onCheckedChange = { ( ) =>
378+ changeRole ( collaborator , "collaborator" )
379+ }
380+ disabled = { isOnlyActiveAdmin }
381+ >
382+ Collaborator
383+ </ DropdownMenu . CheckboxItem >
384+ </ DropdownMenu . Group >
385+ < DropdownMenu . Separator />
386+
387+ < DropdownMenu . Item
388+ onSelect = { ( ) => {
389+ removeCollaborator ( collaborator ) ;
390+ } }
391+ disabled = { isCurrentUser || isOnlyActiveAdmin }
392+ >
393+ Remove access
394+ </ DropdownMenu . Item >
395+ </ DropdownMenu . Content >
396+ </ DropdownMenu . Root >
397+ </ Table . Cell >
395398 </ Table . Row >
396399 ) ;
397400 } ) }
398401 </ Table . Body >
399402 </ Table . Root >
400403 < Flex justify = "end" mt = "5" gap = "3" align = "center" >
401- { isEditable && (
404+ { hasEdited && (
402405 < >
403- { hasEdited && (
404- < >
405- < Text size = "2" color = "gray" className = "italic" >
406- Pending changes
407- </ Text >
408- < Button
409- variant = "outline"
410- size = "2"
411- onClick = { ( ) => {
412- collaboratorsRef . current = collaborators ;
413- setDraftCollaborators ( collaborators ) ;
414- } }
406+ < Text size = "2" color = "gray" className = "italic" >
407+ Pending changes
408+ </ Text >
409+ < Button
410+ variant = "outline"
411+ size = "2"
412+ onClick = { ( ) => {
413+ collaboratorsRef . current = collaborators ;
414+ setDraftCollaborators ( collaborators ) ;
415+ } }
416+ disabled = { ! isCurrentUserAdmin }
417+ >
418+ Cancel
419+ </ Button >
420+ </ >
421+ ) }
422+
423+ < AlertDialog . Root >
424+ < AlertDialog . Trigger >
425+ < Button
426+ highContrast
427+ size = "2"
428+ loading = { isSubmitting }
429+ disabled = { ! isCurrentUserAdmin || ! hasEdited || isSubmitting }
430+ >
431+ Save
432+ </ Button >
433+ </ AlertDialog . Trigger >
434+ < AlertDialog . Content maxWidth = "450px" >
435+ < AlertDialog . Title > Save changes?</ AlertDialog . Title >
436+ < AlertDialog . Description size = "2" >
437+ < Text > Are you sure you want to save these changes?</ Text >
438+ < br />
439+ < br />
440+ { warnings . length > 0 && (
441+ < div
442+ className = { `prose prose-sm ${
443+ appearance === "dark" ? "prose-invert" : ""
444+ } `}
415445 >
446+ < ul style = { { marginTop : "0" , paddingLeft : "16px" } } >
447+ { warnings . map ( ( warning , index ) => (
448+ < li key = { index } style = { { marginBottom : "4px" } } >
449+ < Text size = "2" > { warning } </ Text >
450+ </ li >
451+ ) ) }
452+ </ ul >
453+ </ div >
454+ ) }
455+ < br />
456+ </ AlertDialog . Description >
457+
458+ < Flex gap = "3" mt = "4" justify = "end" >
459+ < AlertDialog . Cancel >
460+ < Button variant = "soft" color = "gray" >
416461 Cancel
417462 </ Button >
418- </ >
419- ) }
420-
421- < AlertDialog . Root >
422- < AlertDialog . Trigger >
423- < Button
424- highContrast
425- size = "2"
426- loading = { isSubmitting }
427- disabled = { ! hasEdited || isSubmitting }
428- >
463+ </ AlertDialog . Cancel >
464+ < AlertDialog . Action
465+ onClick = { ( ) => {
466+ if ( ! boardId ) return ;
467+
468+ fetcher . submit (
469+ {
470+ collaborators : draftCollaborators . map ( ( c ) => ( {
471+ userId : c . userId ,
472+ role : c . role ,
473+ } ) ) ,
474+ } ,
475+ {
476+ encType : "application/json" ,
477+ method : "POST" ,
478+ action : `/boards/${ boardId } /collaborators/update` ,
479+ }
480+ ) ;
481+ } }
482+ >
483+ < Button variant = "solid" highContrast loading = { isSubmitting } >
429484 Save
430485 </ Button >
431- </ AlertDialog . Trigger >
432- < AlertDialog . Content maxWidth = "450px" >
433- < AlertDialog . Title > Save changes?</ AlertDialog . Title >
434- < AlertDialog . Description size = "2" >
435- < Text > Are you sure you want to save these changes?</ Text >
436- < br />
437- < br />
438- { warnings . length > 0 && (
439- < div
440- className = { `prose prose-sm ${
441- appearance === "dark" ? "prose-invert" : ""
442- } `}
443- >
444- < ul style = { { marginTop : "0" , paddingLeft : "16px" } } >
445- { warnings . map ( ( warning , index ) => (
446- < li key = { index } style = { { marginBottom : "4px" } } >
447- < Text size = "2" > { warning } </ Text >
448- </ li >
449- ) ) }
450- </ ul >
451- </ div >
452- ) }
453- < br />
454- </ AlertDialog . Description >
455-
456- < Flex gap = "3" mt = "4" justify = "end" >
457- < AlertDialog . Cancel >
458- < Button variant = "soft" color = "gray" >
459- Cancel
460- </ Button >
461- </ AlertDialog . Cancel >
462- < AlertDialog . Action
463- onClick = { ( ) => {
464- if ( ! boardId ) return ;
465-
466- fetcher . submit (
467- {
468- collaborators : draftCollaborators . map ( ( c ) => ( {
469- userId : c . userId ,
470- role : c . role ,
471- } ) ) ,
472- } ,
473- {
474- encType : "application/json" ,
475- method : "POST" ,
476- action : `/boards/${ boardId } /collaborators/update` ,
477- }
478- ) ;
479- } }
480- >
481- < Button variant = "solid" highContrast loading = { isSubmitting } >
482- Save
483- </ Button >
484- </ AlertDialog . Action >
485- </ Flex >
486- </ AlertDialog . Content >
487- </ AlertDialog . Root >
488- </ >
489- ) }
486+ </ AlertDialog . Action >
487+ </ Flex >
488+ </ AlertDialog . Content >
489+ </ AlertDialog . Root >
490490 </ Flex >
491491 </ >
492492 ) ;
0 commit comments