From ccf213f8672db17bc697cf10f126ba24834e126f Mon Sep 17 00:00:00 2001 From: Greg Trihus Date: Thu, 7 May 2026 11:45:43 -0500 Subject: [PATCH 1/2] TT-7184 Enhance UserActionCell and ProfileDialog for offline handling - Added memberActionsEnabled prop to UserActionCell to control edit/delete button states based on offline status. - Updated ProfileDialog to set read-only mode when offline or in specific edit scenarios. - Introduced tests for UserActionCell to verify button states under different conditions. --- src/renderer/src/components/ProfileDialog.tsx | 14 +++++ .../src/components/UserActionCell.test.tsx | 58 +++++++++++++++++++ .../src/components/UserActionCell.tsx | 17 ++++-- src/renderer/src/components/UserTable.tsx | 13 ++++- 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 src/renderer/src/components/UserActionCell.test.tsx diff --git a/src/renderer/src/components/ProfileDialog.tsx b/src/renderer/src/components/ProfileDialog.tsx index 4726907a..5f7a44dd 100644 --- a/src/renderer/src/components/ProfileDialog.tsx +++ b/src/renderer/src/components/ProfileDialog.tsx @@ -790,7 +790,21 @@ export function ProfileDialog(props: ProfileDialogProps) { useEffect(() => setReadOnly(mode === 'viewMyAccount'), [mode]); + useEffect(() => { + if (mode !== 'editMember' || !editId || /Add/i.test(editId)) return; + if (isOffline || offlineOnly) setReadOnly(true); + else setReadOnly(false); + }, [mode, editId, isOffline, offlineOnly, open]); + const onEditClicked = () => { + if ( + mode === 'editMember' && + editId && + !/Add/i.test(editId) && + (isOffline || offlineOnly) + ) { + return; + } setReadOnly(false); }; diff --git a/src/renderer/src/components/UserActionCell.test.tsx b/src/renderer/src/components/UserActionCell.test.tsx new file mode 100644 index 00000000..c690c190 --- /dev/null +++ b/src/renderer/src/components/UserActionCell.test.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import UserActionCell from './UserActionCell'; +import type { GridRenderCellParams } from '@mui/x-data-grid'; +import type { IRow } from './UserTable'; + +jest.mock('../context/useGlobal', () => ({ + useGlobal: (key: string) => + key === 'user' ? ['admin-id', jest.fn()] : [null, jest.fn()], +})); + +jest.mock('../crud/useRole', () => ({ + useRole: () => ({ userIsAdmin: true }), +})); + +const baseParams = { + id: 'other-user', + field: 'action', + value: 'other-user', + row: { id: 'other-user' } as IRow, + colDef: { field: 'action' }, + api: {} as GridRenderCellParams['api'], + hasFocus: false, + tabIndex: 0, +} as GridRenderCellParams; + +describe('UserActionCell', () => { + it('disables edit and delete when memberActionsEnabled is false (offline / offline-only)', () => { + render( + () => {}} + handleDelete={() => () => {}} + admins={[{ id: 'other-user', role: 'Admin' } as IRow]} + memberActionsEnabled={false} + /> + ); + expect(screen.getByLabelText('edit-other-user')).toBeDisabled(); + expect(screen.getByLabelText('del-other-user')).toBeDisabled(); + }); + + it('allows edit and delete for another user when admin and memberActionsEnabled', () => { + render( + () => {}} + handleDelete={() => () => {}} + admins={[ + { id: 'admin-id', role: 'Admin' } as IRow, + { id: 'other-user', role: 'Admin' } as IRow, + ]} + memberActionsEnabled + /> + ); + expect(screen.getByLabelText('edit-other-user')).not.toBeDisabled(); + expect(screen.getByLabelText('del-other-user')).not.toBeDisabled(); + }); +}); diff --git a/src/renderer/src/components/UserActionCell.tsx b/src/renderer/src/components/UserActionCell.tsx index bd73e4b9..12ac8a64 100644 --- a/src/renderer/src/components/UserActionCell.tsx +++ b/src/renderer/src/components/UserActionCell.tsx @@ -10,11 +10,19 @@ interface IProps { handleEdit: (userId: string) => () => void; handleDelete: (value: string) => () => void; admins: IRow[]; + /** When false (offline or desktop offline-only), member row actions must stay disabled — local changes are not synced. */ + memberActionsEnabled?: boolean; } export default function PlayCell(params: GridRenderCellParams & IProps) { const [user] = useGlobal('user'); - const { value, handleEdit, handleDelete, admins } = params; + const { + value, + handleEdit, + handleDelete, + admins, + memberActionsEnabled = true, + } = params; const { userIsAdmin } = useRole(); const isCurrentUser = (userId: string) => userId === user; @@ -27,7 +35,7 @@ export default function PlayCell(params: GridRenderCellParams & IProps) { aria-label={'edit-' + value} color="default" onClick={handleEdit(value)} - disabled={isCurrentUser(value)} + disabled={!memberActionsEnabled || isCurrentUser(value)} > @@ -38,9 +46,10 @@ export default function PlayCell(params: GridRenderCellParams & IProps) { color="default" onClick={handleDelete(value)} disabled={ - userIsAdmin + !memberActionsEnabled || + (userIsAdmin ? admins.length === 1 && isCurrentUser(value) - : !isCurrentUser(value) + : !isCurrentUser(value)) } > diff --git a/src/renderer/src/components/UserTable.tsx b/src/renderer/src/components/UserTable.tsx index 80cfb72f..c52b6425 100644 --- a/src/renderer/src/components/UserTable.tsx +++ b/src/renderer/src/components/UserTable.tsx @@ -185,9 +185,14 @@ export function UserTable() { () => data.filter((d) => d.role === RoleNames.Admin), [data] ); + /** Team membership changes (invite, add, edit, remove) only apply locally when offline; they are not synced to the server. */ + const memberActionsEnabled = useMemo( + () => !offline && !offlineOnly, + [offline, offlineOnly] + ); const canEdit = useMemo( - () => userIsAdmin && (!offline || offlineOnly), - [userIsAdmin, offline, offlineOnly] + () => userIsAdmin && memberActionsEnabled, + [userIsAdmin, memberActionsEnabled] ); const columns: GridColDef[] = [ @@ -198,7 +203,8 @@ export function UserTable() { { field: 'role', headerName: ts.teamrole, width: 100 }, { field: 'action', - headerName: userIsAdmin ? t.action : '\u00A0', + headerName: + userIsAdmin && memberActionsEnabled ? t.action : '\u00A0', width: 150, sortable: false, filterable: false, @@ -208,6 +214,7 @@ export function UserTable() { handleEdit={handleEdit} handleDelete={handleDelete} admins={admins} + memberActionsEnabled={memberActionsEnabled} /> ), }, From a50332a2240495ba8e6d938468a173d1b9025eed Mon Sep 17 00:00:00 2001 From: Greg Trihus Date: Thu, 7 May 2026 11:45:43 -0500 Subject: [PATCH 2/2] Refactor ProfileDialog and UserTable for improved offline handling - Updated ProfileDialog to correctly set read-only mode based on offline status and edit conditions. - Enhanced UserTable to filter admin roles more accurately and clarified the conditions under which team membership changes are allowed. - Adjusted button states in UserTable to reflect the new offline handling logic. --- src/renderer/src/components/ProfileDialog.tsx | 20 +++++++++---------- src/renderer/src/components/UserTable.tsx | 20 +++++++++++++------ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/components/ProfileDialog.tsx b/src/renderer/src/components/ProfileDialog.tsx index 5f7a44dd..579ae303 100644 --- a/src/renderer/src/components/ProfileDialog.tsx +++ b/src/renderer/src/components/ProfileDialog.tsx @@ -792,19 +792,19 @@ export function ProfileDialog(props: ProfileDialogProps) { useEffect(() => { if (mode !== 'editMember' || !editId || /Add/i.test(editId)) return; - if (isOffline || offlineOnly) setReadOnly(true); + if (isOffline && !offlineOnly) setReadOnly(true); else setReadOnly(false); }, [mode, editId, isOffline, offlineOnly, open]); + const memberEditOfflineBlocked = + mode === 'editMember' && + !!editId && + !/Add/i.test(editId) && + isOffline && + !offlineOnly; + const onEditClicked = () => { - if ( - mode === 'editMember' && - editId && - !/Add/i.test(editId) && - (isOffline || offlineOnly) - ) { - return; - } + if (memberEditOfflineBlocked) return; setReadOnly(false); }; @@ -863,7 +863,7 @@ export function ProfileDialog(props: ProfileDialogProps) { {email || ''} {((editId && /Add/i.test(editId)) || !userNotComplete()) && (