From 699db1f68b7ba8cc2a59410d0008e1fe06d53030 Mon Sep 17 00:00:00 2001 From: Greg Trihus Date: Fri, 12 Dec 2025 15:53:43 -0600 Subject: [PATCH 1/2] TT-6955 Enhance OrgHead component to manage admin roles and update button visibility This update modifies the OrgHead component to incorporate user role management, allowing for differentiated button visibility based on whether the user is an admin. The changes include: - Added logic to create mock memory with admin and member roles. - Updated tests to verify button visibility for admin and non-admin users on the team screen. - Refactored the OrgHead component to conditionally render settings and members buttons based on the user`s admin status. These enhancements improve the user experience by ensuring that only relevant options are presented to users based on their roles within the organization. --- .../src/components/App/OrgHead.cy.tsx | 130 +++++++++++++++--- src/renderer/src/components/App/OrgHead.tsx | 14 +- 2 files changed, 122 insertions(+), 22 deletions(-) diff --git a/src/renderer/src/components/App/OrgHead.cy.tsx b/src/renderer/src/components/App/OrgHead.cy.tsx index 4697474..2285b88 100644 --- a/src/renderer/src/components/App/OrgHead.cy.tsx +++ b/src/renderer/src/components/App/OrgHead.cy.tsx @@ -19,10 +19,59 @@ import { OrganizationD } from '@model/organization'; // Mock memory with query function that can return organization data // The findRecord function uses: memory.cache.query((q) => q.findRecord({ type, id })) // The findRecords function uses: memory.cache.query((q) => q.findRecords(type)) -const createMockMemory = (orgData?: OrganizationD): Memory => { +const createMockMemory = ( + orgData?: OrganizationD, + isAdmin: boolean = false, + userId: string = 'test-user-id' +): Memory => { // Store organizations in an array for findRecords queries const organizations = orgData ? [orgData] : []; + // Create role records + const adminRoleId = 'admin-role-id'; + const memberRoleId = 'member-role-id'; + const roles = [ + { + id: adminRoleId, + type: 'role', + attributes: { + roleName: 'Admin', + orgRole: true, + }, + keys: { remoteId: 'admin-role-remote-id' }, + }, + { + id: memberRoleId, + type: 'role', + attributes: { + roleName: 'Member', + orgRole: true, + }, + keys: { remoteId: 'member-role-remote-id' }, + }, + ]; + + // Create organization membership record if orgData exists + const orgMemberships = orgData + ? [ + { + id: 'org-membership-id', + type: 'organizationmembership', + relationships: { + user: { data: { type: 'user', id: userId } }, + organization: { data: { type: 'organization', id: orgData.id } }, + role: { + data: { + type: 'role', + id: isAdmin ? adminRoleId : memberRoleId, + }, + }, + }, + keys: { remoteId: 'org-membership-remote-id' }, + }, + ] + : []; + // Create a mock query builder with both findRecord and findRecords const createMockQueryBuilder = () => ({ findRecord: ({ type, id }: { type: string; id: string }) => { @@ -30,7 +79,12 @@ const createMockMemory = (orgData?: OrganizationD): Memory => { if (type === 'organization' && id === orgData?.id && orgData) { return orgData; } - // Return undefined for other record types (user, role, etc.) + // Return role records + if (type === 'role') { + const role = roles.find((r) => r.id === id); + if (role) return role; + } + // Return undefined for other record types (user, etc.) return undefined; }, findRecords: (type: string) => { @@ -39,8 +93,15 @@ const createMockMemory = (orgData?: OrganizationD): Memory => { if (type === 'organization') { return organizations; } - // Return empty array for other types (roles, organizationmembership, etc.) - // This is used by useRole for roles, organizationmembership, etc. + // Return roles array when querying for 'role' type + if (type === 'role') { + return roles; + } + // Return organization memberships when querying for 'organizationmembership' type + if (type === 'organizationmembership') { + return orgMemberships; + } + // Return empty array for other types return []; }, }); @@ -180,7 +241,8 @@ describe('OrgHead', () => { initialState: ReturnType, initialEntries: string[] = ['/team'], orgId?: string, - orgData?: OrganizationD + orgData?: OrganizationD, + isAdmin: boolean = false ) => { // Set organization ID in localStorage if provided if (orgId) { @@ -189,8 +251,10 @@ describe('OrgHead', () => { }); } - // Create memory with org data if provided - const memory = orgData ? createMockMemory(orgData) : createMockMemory(); + // Create memory with org data and admin status if provided + const memory = orgData + ? createMockMemory(orgData, isAdmin, initialState.user) + : createMockMemory(undefined, false, initialState.user); // Create stubs for TeamContext methods const mockTeamUpdate = cy.stub().as('teamUpdate'); @@ -316,14 +380,14 @@ describe('OrgHead', () => { cy.get('h6, [variant="h6"]').should('be.visible'); }); - it('should show settings and members buttons when on team screen', () => { + it('should show settings and members buttons when on team screen and user is admin', () => { const orgId = 'test-org-id'; const orgName = 'Test Organization'; const orgData = createMockOrganization(orgId, orgName); - mountOrgHead(createInitialState(), ['/team'], orgId, orgData); + mountOrgHead(createInitialState(), ['/team'], orgId, orgData, true); - // Check for settings and members icon buttons + // Check for settings and members icon buttons (both should be visible for admin) // MUI IconButtons contain SVG icons as children cy.get('button').should('have.length.at.least', 2); // Verify that buttons contain SVG elements (icon buttons should have SVG children) @@ -331,6 +395,19 @@ describe('OrgHead', () => { cy.get('button svg').should('have.length.at.least', 2); }); + it('should show only members button (not settings) when on team screen and user is not admin', () => { + const orgId = 'test-org-id'; + const orgName = 'Test Organization'; + const orgData = createMockOrganization(orgId, orgName); + + mountOrgHead(createInitialState(), ['/team'], orgId, orgData, false); + + // Should only have members button, not settings button + cy.get('button').should('have.length', 1); + cy.get('button').should('be.visible'); + cy.get('button svg').should('have.length', 1); + }); + it('should not show settings and members buttons when not on team screen', () => { const orgId = 'test-org-id'; const orgName = 'Test Organization'; @@ -343,30 +420,30 @@ describe('OrgHead', () => { cy.get('button').should('not.exist'); }); - it('should open TeamDialog when settings button is clicked', () => { + it('should open TeamDialog when settings button is clicked (admin only)', () => { const orgId = 'test-org-id'; const orgName = 'Test Organization'; const orgData = createMockOrganization(orgId, orgName); - mountOrgHead(createInitialState(), ['/team'], orgId, orgData); + mountOrgHead(createInitialState(), ['/team'], orgId, orgData, true); // Find and click the first button (settings button) - // The settings button is the first IconButton rendered + // The settings button is the first IconButton rendered (only visible for admin) cy.get('button').first().click(); // TeamDialog should be open (check for a dialog or form element) cy.get('[role="dialog"]').should('be.visible'); }); - it('should open members dialog when members button is clicked', () => { + it('should open members dialog when members button is clicked (admin)', () => { const orgId = 'test-org-id'; const orgName = 'Test Organization'; const orgData = createMockOrganization(orgId, orgName); - mountOrgHead(createInitialState(), ['/team'], orgId, orgData); + mountOrgHead(createInitialState(), ['/team'], orgId, orgData, true); // Find and click the second button (members button) - // The members button is the second IconButton rendered + // The members button is the second IconButton rendered (when admin) cy.get('button').eq(1).click(); // BigDialog should be open with members title @@ -374,14 +451,29 @@ describe('OrgHead', () => { cy.get('[role="dialog"]').should('be.visible'); }); - it('should close TeamDialog when editOpen is set to false', () => { + it('should open members dialog when members button is clicked (non-admin)', () => { const orgId = 'test-org-id'; const orgName = 'Test Organization'; const orgData = createMockOrganization(orgId, orgName); - mountOrgHead(createInitialState(), ['/team'], orgId, orgData); + mountOrgHead(createInitialState(), ['/team'], orgId, orgData, false); + + // Find and click the button (members button - only button for non-admin) + cy.get('button').first().click(); + + // BigDialog should be open with members title + cy.contains('Members of Test Organization').should('be.visible'); + cy.get('[role="dialog"]').should('be.visible'); + }); + + it('should close TeamDialog when editOpen is set to false (admin only)', () => { + const orgId = 'test-org-id'; + const orgName = 'Test Organization'; + const orgData = createMockOrganization(orgId, orgName); + + mountOrgHead(createInitialState(), ['/team'], orgId, orgData, true); - // Open the dialog + // Open the dialog (settings button is first for admin) cy.get('button').first().click(); cy.get('[role="dialog"]').should('be.visible'); diff --git a/src/renderer/src/components/App/OrgHead.tsx b/src/renderer/src/components/App/OrgHead.tsx index 3d6959f..2a9ee81 100644 --- a/src/renderer/src/components/App/OrgHead.tsx +++ b/src/renderer/src/components/App/OrgHead.tsx @@ -37,6 +37,7 @@ export const OrgHead = () => { const { pathname } = useLocation(); const isTeamScreen = pathname.includes('/team'); const isSwitchTeamsScreen = pathname.includes('/switch-teams'); + const { userIsOrgAdmin } = useRole(); const ctx = useContext(TeamContext); const { teamDelete } = ctx?.state ?? {}; const { setMyOrgRole } = useRole(); @@ -52,6 +53,11 @@ export const OrgHead = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [orgId, organizations]); + const isAdmin = useMemo( + () => userIsOrgAdmin(orgId ?? ''), + [orgId, userIsOrgAdmin] + ); + const handleSettings = () => { setEditOpen(true); }; @@ -99,9 +105,11 @@ export const OrgHead = () => { {isTeamScreen && ( <> - - - + {isAdmin && ( + + + + )} {orgRec && ( From 0deedcf2c014ec6f6c1a58ff1835712aaf44ef64 Mon Sep 17 00:00:00 2001 From: gtryus Date: Fri, 12 Dec 2025 16:01:10 -0600 Subject: [PATCH 2/2] Update src/renderer/src/components/App/OrgHead.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/renderer/src/components/App/OrgHead.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/renderer/src/components/App/OrgHead.tsx b/src/renderer/src/components/App/OrgHead.tsx index 2a9ee81..39cb244 100644 --- a/src/renderer/src/components/App/OrgHead.tsx +++ b/src/renderer/src/components/App/OrgHead.tsx @@ -37,10 +37,9 @@ export const OrgHead = () => { const { pathname } = useLocation(); const isTeamScreen = pathname.includes('/team'); const isSwitchTeamsScreen = pathname.includes('/switch-teams'); - const { userIsOrgAdmin } = useRole(); + const { userIsOrgAdmin, setMyOrgRole } = useRole(); const ctx = useContext(TeamContext); const { teamDelete } = ctx?.state ?? {}; - const { setMyOrgRole } = useRole(); const orgId = useMemo( () => localStorage.getItem(localUserKey(LocalKey.team)),