Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/renderer/src/components/ProfileDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 memberEditOfflineBlocked =
mode === 'editMember' &&
!!editId &&
!/Add/i.test(editId) &&
isOffline &&
!offlineOnly;

const onEditClicked = () => {
if (memberEditOfflineBlocked) return;
setReadOnly(false);
};

Expand Down Expand Up @@ -849,7 +863,7 @@ export function ProfileDialog(props: ProfileDialogProps) {
<Caption sx={profileEmailProps}>{email || ''}</Caption>
{((editId && /Add/i.test(editId)) || !userNotComplete()) && (
<Button
disabled={!readOnly}
disabled={!readOnly || memberEditOfflineBlocked}
variant="contained"
onClick={onEditClicked}
sx={editProfileProps}
Expand Down
58 changes: 58 additions & 0 deletions src/renderer/src/components/UserActionCell.test.tsx
Original file line number Diff line number Diff line change
@@ -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<IRow>['api'],
hasFocus: false,
tabIndex: 0,
} as GridRenderCellParams<IRow>;

describe('UserActionCell', () => {
it('disables edit and delete when memberActionsEnabled is false (offline / offline-only)', () => {
render(
<UserActionCell
{...baseParams}
handleEdit={() => () => {}}
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(
<UserActionCell
{...baseParams}
handleEdit={() => () => {}}
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();
});
});
17 changes: 13 additions & 4 deletions src/renderer/src/components/UserActionCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IRow> & 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;
Expand All @@ -27,7 +35,7 @@ export default function PlayCell(params: GridRenderCellParams<IRow> & IProps) {
aria-label={'edit-' + value}
color="default"
onClick={handleEdit(value)}
disabled={isCurrentUser(value)}
disabled={!memberActionsEnabled || isCurrentUser(value)}
>
<EditIcon />
</IconButton>
Expand All @@ -38,9 +46,10 @@ export default function PlayCell(params: GridRenderCellParams<IRow> & IProps) {
color="default"
onClick={handleDelete(value)}
disabled={
userIsAdmin
!memberActionsEnabled ||
(userIsAdmin
? admins.length === 1 && isCurrentUser(value)
: !isCurrentUser(value)
: !isCurrentUser(value))
}
>
<DeleteIcon />
Expand Down
25 changes: 20 additions & 5 deletions src/renderer/src/components/UserTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,26 @@ export function UserTable() {
}, [organization, users, roles, organizationMemberships]);

const admins = useMemo(
() => data.filter((d) => d.role === RoleNames.Admin),
[data]
() =>
data.filter((d) => {
const rec = organizationMemberships.find(
(om) =>
related(om, 'user') === d.id &&
related(om, 'organization') === organization
);
const role = roles.find((r) => r.id === related(rec, 'role'));
return role && role.attributes.roleName === RoleNames.Admin;
}),
[data, organizationMemberships, roles, organization]
);
/** Team membership changes are allowed when online, or when running as an offline-only desktop (local-first). They are blocked only when an online session has lost connectivity, since changes could not be synced. */
const memberActionsEnabled = useMemo(
() => !offline || offlineOnly,
[offline, offlineOnly]
);
const canEdit = useMemo(
() => userIsAdmin && (!offline || offlineOnly),
[userIsAdmin, offline, offlineOnly]
() => userIsAdmin && memberActionsEnabled,
[userIsAdmin, memberActionsEnabled]
);

const columns: GridColDef<IRow>[] = [
Expand All @@ -198,7 +212,7 @@ 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,
Expand All @@ -208,6 +222,7 @@ export function UserTable() {
handleEdit={handleEdit}
handleDelete={handleDelete}
admins={admins}
memberActionsEnabled={memberActionsEnabled}
/>
),
},
Expand Down
Loading