Conversation
My preferred (implemented) solution to the issue:
|
|
adding self review comments next. |
| invitedOrganizations OrganizationInvite[] | ||
| createdSelfServeInvites OrganizationSelfServeInvite[] | ||
| notes Note[] | ||
| boardShares BoardShare[] |
There was a problem hiding this comment.
links users to boards they can access
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
| notes Note[] | ||
| shares BoardShare[] |
There was a problem hiding this comment.
links boards to users who have access
| model BoardShare { | ||
| id String @id @default(cuid()) | ||
| boardId String | ||
| userId String | ||
| board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) | ||
| user User @relation(fields: [userId], references: [id], onDelete: Cascade) | ||
| createdAt DateTime @default(now()) | ||
|
|
||
| @@unique([boardId, userId]) | ||
| @@map("board_shares") | ||
| @@index([boardId]) | ||
| @@index([userId]) | ||
| } | ||
|
|
There was a problem hiding this comment.
Added BoardShare table to track which users can access which boards like a permission list between users and boards
| const boards = await db.board.findMany({ | ||
| where: { organizationId: user.organizationId }, | ||
| where: { | ||
| OR: [ | ||
| { isPublic: true }, | ||
| { | ||
| shares: { | ||
| some: { | ||
| userId: session.user.id, | ||
| }, | ||
| }, | ||
| }, | ||
| { createdBy: session.user.id }, // Boards created by the user | ||
| ], | ||
| }, |
There was a problem hiding this comment.
update board listing. now include three access patterns - public boards (anyone), explicitly shared boards (has BoardShare record), and user created boards (createdBy matches user)
ensures creators never lose access to their own content while maintaining granular control
| organization: { | ||
| select: { | ||
| id: true, | ||
| name: true, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
add organization info to response. needed for UI to show org context, was missing before and caused display issues
| <p className="text-sm text-zinc-600 dark:text-zinc-400">{member.email}</p> | ||
| {memberSharing && ( | ||
| <div className="flex items-center space-x-2 mt-1"> | ||
| <span className="text-xs text-zinc-500 dark:text-zinc-400"> | ||
| {memberSharing.shareAllBoards | ||
| ? `All ${memberSharing.totalBoards} boards shared` | ||
| : `${memberSharing.sharedBoardIds.length} of ${memberSharing.totalBoards} boards shared`} | ||
| </span> |
There was a problem hiding this comment.
shows either all boards are shared or what number of boards are shared
| {/* Board sharing toggle - only for admins and not for yourself */} | ||
| {user?.isAdmin && member.id !== user.id && ( | ||
| <div className="flex items-center space-x-2"> | ||
| <Switch | ||
| checked={memberSharing?.shareAllBoards || false} | ||
| onCheckedChange={(checked) => | ||
| handleToggleShareAllBoards(member.id, checked) | ||
| } | ||
| disabled={updatingSharing === member.id} | ||
| className="data-[state=checked]:bg-green-600" | ||
| /> | ||
| <span className="text-sm text-zinc-600 dark:text-zinc-400"> | ||
| Share all boards | ||
| </span> | ||
| <Button | ||
| onClick={() => | ||
| openBoardDialog( | ||
| member.id, | ||
| member.name || member.email, | ||
| memberSharing?.sharedBoardIds || [] | ||
| ) | ||
| } | ||
| variant="outline" | ||
| size="sm" | ||
| className="text-zinc-500 dark:text-zinc-400 hover:text-zinc-600 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-800" | ||
| title="View and edit shared boards" | ||
| > | ||
| <Eye className="w-4 h-4" /> | ||
| </Button> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
board sharing controls section,
only shows for org admins and not for the current user, provides toggle for "Share all boards" and "View (Eye)" button for granular control
| {/* Admin toggle - only for admins and not for yourself */} | ||
| {user?.isAdmin && member.id !== user.id && ( | ||
| <> | ||
| <Button | ||
| onClick={() => handleToggleAdmin(member.id, !!member.isAdmin)} | ||
| variant="outline" | ||
| size="sm" | ||
| className={`${ | ||
| member.isAdmin | ||
| ? "text-purple-600 hover:text-purple-700 hover:bg-purple-50 dark:text-purple-400 dark:hover:text-purple-300 dark:hover:bg-purple-900" | ||
| : "text-zinc-500 dark:text-zinc-400 hover:text-purple-600 hover:bg-purple-50 dark:hover:text-purple-300 dark:hover:bg-purple-900" | ||
| }`} | ||
| title={member.isAdmin ? "Remove admin role" : "Make admin"} | ||
| > | ||
| {member.isAdmin ? ( | ||
| <ShieldCheck className="w-4 h-4" /> | ||
| ) : ( | ||
| <Shield className="w-4 h-4" /> | ||
| )} | ||
| </Button> | ||
| <Button | ||
| onClick={() => handleRemoveMember(member.id, member.name || member.email)} | ||
| variant="outline" | ||
| size="sm" | ||
| className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-900" | ||
| > | ||
| <Trash2 className="w-4 h-4" /> | ||
| </Button> | ||
| </> | ||
| )} | ||
| </div> | ||
| </div> | ||
| <div className="flex items-center space-x-2"> | ||
| {/* Only show admin toggle to current admins and not for yourself */} | ||
| {user?.isAdmin && member.id !== user.id && ( | ||
| <Button | ||
| onClick={() => handleToggleAdmin(member.id, !!member.isAdmin)} | ||
| variant="outline" | ||
| size="sm" | ||
| className={`${ | ||
| member.isAdmin | ||
| ? "text-purple-600 hover:text-purple-700 hover:bg-purple-50 dark:text-purple-400 dark:hover:text-purple-300 dark:hover:bg-purple-900" | ||
| : "text-zinc-500 dark:text-zinc-400 hover:text-purple-600 hover:bg-purple-50 dark:hover:text-purple-300 dark:hover:bg-purple-900" | ||
| }`} | ||
| title={member.isAdmin ? "Remove admin role" : "Make admin"} | ||
| > | ||
| {member.isAdmin ? ( | ||
| <ShieldCheck className="w-4 h-4" /> | ||
| ) : ( | ||
| <Shield className="w-4 h-4" /> | ||
| )} | ||
| </Button> | ||
| )} | ||
| {user?.isAdmin && member.id !== user.id && ( | ||
| <Button | ||
| onClick={() => handleRemoveMember(member.id, member.name || member.email)} | ||
| variant="outline" | ||
| size="sm" | ||
| className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-900" | ||
| > | ||
| <Trash2 className="w-4 h-4" /> | ||
| </Button> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ))} | ||
| ); | ||
| })} |
There was a problem hiding this comment.
admin controls section.
existing admin toggle and remove member functionality,
moved into conditional rendering block
| </AlertDialog> | ||
|
|
||
| {/* Board Sharing Dialog */} | ||
| <Dialog |
There was a problem hiding this comment.
unable to select full dialog component, github error
There was a problem hiding this comment.
add e2e tests for board sharing:
- Board Creation & Creator Access:
- admin creates board with auto-access
- creator can access own board
- Board Sharing Dialog
- sharing dialog shows creator identification
- sharing dialog shows all members
- dialog persists during member toggles
- dialog closes on Done button
- Access Controls
- shared user can access board
- creator has access without explicit sharing
- Organization Integration
- org sharing API works
- Edge Cases
- public boards are accessible
- board deletion removes sharing
|
@slavingia can you review this now? |

fixes #822
Board-Level Sharing Permissions
Implemented granular board sharing controls that allow organization admins to share specific boards with team members, rather than giving access to all boards.
Approach
Share all boardsoptionView (Eye)button for precise controlgumboard.board.sharing.mp4
Changes
BoardSharemodel for granular permissionsAI Disclosure
Self Review
Screenshots
Org Settings
Board Settings