@cccsaurora/howler-ui 3.1.0-dev.1643 → 3.1.0-dev.1647

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
- export type PermissionData = {
1
+ export type PermissionEntry = {
2
2
  privilege: string;
3
- user_ids: string[];
3
+ user_id: string;
4
4
  };
5
+ export type PermissionData = PermissionEntry[];
5
6
  type ParentUri = (id: string) => string;
6
7
  declare const createPermissionsApi: <T>(parentUri: ParentUri) => {
7
8
  put: (id: string, data: PermissionData) => Promise<T | undefined>;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { render, screen, waitFor, within } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
@@ -51,7 +51,7 @@ vi.mock('./display/HowlerAvatar', () => ({
51
51
  default: ({ userId }) => _jsx("span", { children: userId })
52
52
  }));
53
53
  vi.mock('./UserList', () => ({
54
- default: ({ onChange }) => (_jsx("button", { onClick: () => onChange(['analyst']), children: "Select analyst" }))
54
+ default: ({ onChange }) => (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => onChange(['analyst']), children: "Select analyst" }), _jsx("button", { onClick: () => onChange(['member']), children: "Select member" })] }))
55
55
  }));
56
56
  describe('MembershipManagement', () => {
57
57
  const entity = {
@@ -67,6 +67,12 @@ describe('MembershipManagement', () => {
67
67
  name: 'Analyst',
68
68
  type: [],
69
69
  username: 'analyst'
70
+ },
71
+ member: {
72
+ email: 'member@example.com',
73
+ name: 'Member',
74
+ type: [],
75
+ username: 'member'
70
76
  }
71
77
  },
72
78
  fetchUsers: vi.fn(),
@@ -106,16 +112,20 @@ describe('MembershipManagement', () => {
106
112
  }
107
113
  });
108
114
  });
109
- it('grants a selected member with the batched permission payload', async () => {
115
+ it('adds a selected user immediately and saves the selected privilege', async () => {
110
116
  const user = userEvent.setup();
111
117
  const onChange = renderMembershipManagement();
112
118
  await user.click(screen.getByRole('button', { name: 'membership.manage' }));
113
119
  await user.click(screen.getByRole('button', { name: 'Select analyst' }));
114
- await user.click(screen.getByRole('button', { name: 'add' }));
115
- expect(permissionPutMock).toHaveBeenCalledWith('action-id', {
116
- privilege: 'members',
117
- user_ids: ['analyst']
118
- });
120
+ await user.click(screen.getByRole('button', { name: 'Select member' }));
121
+ const roleSelects = screen.getAllByRole('combobox');
122
+ await user.click(roleSelects[0]);
123
+ await user.click(screen.getByRole('option', { name: 'membership.privilege.admins' }));
124
+ await user.click(screen.getByRole('button', { name: 'button.save' }));
125
+ expect(permissionPutMock).toHaveBeenCalledWith('action-id', [
126
+ { privilege: 'admins', user_id: 'analyst' },
127
+ { privilege: 'members', user_id: 'member' }
128
+ ]);
119
129
  await waitFor(() => expect(onChange).toHaveBeenCalledWith({ owner: 'owner', admins: [], members: ['analyst'] }));
120
130
  expect(showSuccessMessageMock).toHaveBeenCalledWith('membership.message.success');
121
131
  });
@@ -126,13 +136,10 @@ describe('MembershipManagement', () => {
126
136
  const analystRow = screen.getByText('Analyst').closest('li');
127
137
  expect(analystRow).not.toBeNull();
128
138
  await user.click(within(analystRow).getByRole('button'));
129
- expect(permissionDeleteMock).toHaveBeenCalledWith('action-id', {
130
- privilege: 'admins',
131
- user_ids: ['analyst']
132
- });
139
+ expect(permissionDeleteMock).toHaveBeenCalledWith('action-id', [{ privilege: 'admins', user_id: 'analyst' }]);
133
140
  expect(dispatchApiMock).toHaveBeenCalledWith({
134
141
  request: 'remove-permission',
135
- data: { privilege: 'admins', user_ids: ['analyst'] }
142
+ data: [{ privilege: 'admins', user_id: 'analyst' }]
136
143
  }, { throwError: false });
137
144
  await waitFor(() => expect(onChange).toHaveBeenCalledWith({ owner: 'owner', admins: [], members: [] }));
138
145
  expect(showSuccessMessageMock).toHaveBeenCalledWith('membership.message.success');
@@ -141,7 +148,9 @@ describe('MembershipManagement', () => {
141
148
  const user = userEvent.setup();
142
149
  renderMembershipManagement();
143
150
  await user.click(screen.getByRole('button', { name: 'membership.manage' }));
144
- expect(screen.getByRole('button', { name: 'membership.privilege.owner' })).toBeInTheDocument();
151
+ await user.click(screen.getByRole('button', { name: 'Select analyst' }));
152
+ await user.click(screen.getAllByRole('combobox')[0]);
153
+ expect(screen.getByRole('option', { name: 'membership.privilege.owner' })).toBeInTheDocument();
145
154
  });
146
155
  it('does not offer owner transfer to a local admin', async () => {
147
156
  const user = userEvent.setup();
@@ -153,7 +162,9 @@ describe('MembershipManagement', () => {
153
162
  });
154
163
  renderMembershipManagement({ admins: ['admin'] });
155
164
  await user.click(screen.getByRole('button', { name: 'membership.manage' }));
156
- expect(screen.queryByRole('button', { name: 'membership.privilege.owner' })).not.toBeInTheDocument();
165
+ await user.click(screen.getByRole('button', { name: 'Select analyst' }));
166
+ await user.click(screen.getAllByRole('combobox')[0]);
167
+ expect(screen.queryByRole('option', { name: 'membership.privilege.owner' })).not.toBeInTheDocument();
157
168
  });
158
169
  it('does not render membership controls for an unrelated user', () => {
159
170
  appUserMock.mockReturnValue({
@@ -171,7 +182,7 @@ describe('MembershipManagement', () => {
171
182
  renderMembershipManagement();
172
183
  await user.click(screen.getByRole('button', { name: 'membership.manage' }));
173
184
  await user.click(screen.getByRole('button', { name: 'Select analyst' }));
174
- await user.click(screen.getByRole('button', { name: 'add' }));
185
+ await user.click(screen.getByRole('button', { name: 'button.save' }));
175
186
  await waitFor(() => expect(showErrorMessageMock).toHaveBeenCalledWith('membership.message.error'));
176
187
  expect(showSuccessMessageMock).not.toHaveBeenCalled();
177
188
  });
@@ -10,5 +10,6 @@ declare const UserList: FC<{
10
10
  avatarHeight?: number;
11
11
  disabled?: boolean;
12
12
  multiple?: boolean;
13
+ showEmptyInput?: boolean;
13
14
  }>;
14
15
  export default UserList;
@@ -16,7 +16,7 @@ const UserEntry = ({ user }) => {
16
16
  columnGap: 1.5
17
17
  }, children: [_jsx(HowlerAvatar, { sx: { gridArea: 'profile', alignSelf: 'center', height: '32px', width: '32px' }, userId: user?.username }), _jsx(Typography, { sx: { gridArea: 'name' }, variant: "body1", children: user?.name }), _jsx(Typography, { sx: { gridArea: 'email' }, variant: "caption", children: user?.email ?? '' })] }));
18
18
  };
19
- const UserList = ({ buttonSx = {}, userIds, onChange, i18nLabel, avatarHeight = 32, multiple = false, disabled = false, variant = 'compact', except = [] }) => {
19
+ const UserList = ({ buttonSx = {}, userIds, onChange, i18nLabel, avatarHeight = 32, multiple = false, disabled = false, variant = 'compact', except = [], showEmptyInput = false }) => {
20
20
  const { t } = useTranslation();
21
21
  const [anchorEl, setAnchorEl] = useState(null);
22
22
  const { users, fetchUsers, searchUsers } = useContext(UserListContext);
@@ -26,7 +26,11 @@ const UserList = ({ buttonSx = {}, userIds, onChange, i18nLabel, avatarHeight =
26
26
  // eslint-disable-next-line react-hooks/exhaustive-deps
27
27
  }, [userIds]);
28
28
  const throttler = useMemo(() => new Throttler(300), []);
29
- const search = (value) => throttler.debounce(() => searchUsers(value));
29
+ const search = (value) => throttler.debounce(() => {
30
+ if (value) {
31
+ searchUsers(value);
32
+ }
33
+ });
30
34
  const renderInput = (params) => (_jsx(TextField, { ...params, autoComplete: "off", label: t(i18nLabel), size: "small" }));
31
35
  const renderOption = (props, optionUserId) => {
32
36
  const { key, ...optionProps } = props;
@@ -46,6 +50,6 @@ const UserList = ({ buttonSx = {}, userIds, onChange, i18nLabel, avatarHeight =
46
50
  } })) : (_jsx(Autocomplete, { ...sharedAutocompleteProps, value: userIds?.[0] ?? null, onChange: (__, option) => {
47
51
  onChange(option ? [option] : []);
48
52
  } }));
49
- return (_jsxs(_Fragment, { children: [variant === 'compact' ? (multiple ? (_jsxs(Stack, { direction: "row", spacing: 0.25, alignItems: "center", children: [_jsx(AvatarGroup, { children: uniq(userIds ?? [null]).map(userId => (_jsx(HowlerAvatar, { userId: userId, sx: { height: avatarHeight, width: avatarHeight } }, userId))) }), _jsx(IconButton, { size: "small", sx: buttonSx, disabled: disabled, onClick: e => setAnchorEl(e.currentTarget), children: _jsx(Add, {}) })] })) : (_jsx(IconButton, { sx: buttonSx, disabled: disabled, onClick: e => setAnchorEl(e.currentTarget), children: _jsx(HowlerAvatar, { userId: userIds[0], sx: { height: avatarHeight, width: avatarHeight } }) }))) : multiple ? (_jsxs(Stack, { width: "100%", spacing: 1, alignItems: "stretch", divider: _jsx(Divider, { flexItem: true }), children: [uniq(userIds ?? [null]).map(userId => (_jsx(UserEntry, { user: users[userId] }, userId))), autocomplete] })) : (_jsxs(Stack, { direction: "row", children: [_jsx(HowlerAvatar, { userId: userIds[0], sx: { height: avatarHeight, width: avatarHeight } }), _jsx(Typography, { children: userIds[0] })] })), variant === 'compact' && (_jsx(Popover, { open: !!anchorEl, onClose: () => setAnchorEl(null), anchorEl: anchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, children: _jsx(Box, { sx: { p: 2 }, children: autocomplete }) }))] }));
53
+ return (_jsxs(_Fragment, { children: [variant === 'compact' ? (multiple ? (_jsxs(Stack, { direction: "row", spacing: 0.25, alignItems: "center", children: [_jsx(AvatarGroup, { children: uniq(userIds ?? [null]).map(userId => (_jsx(HowlerAvatar, { userId: userId, sx: { height: avatarHeight, width: avatarHeight } }, userId))) }), _jsx(IconButton, { size: "small", sx: buttonSx, disabled: disabled, onClick: e => setAnchorEl(e.currentTarget), children: _jsx(Add, {}) })] })) : (_jsx(IconButton, { sx: buttonSx, disabled: disabled, onClick: e => setAnchorEl(e.currentTarget), children: _jsx(HowlerAvatar, { userId: userIds[0], sx: { height: avatarHeight, width: avatarHeight } }) }))) : multiple ? (_jsxs(Stack, { width: "100%", spacing: 1, alignItems: "stretch", divider: _jsx(Divider, { flexItem: true }), children: [uniq(userIds ?? [null]).map(userId => (_jsx(UserEntry, { user: users[userId] }, userId))), autocomplete] })) : userIds.length > 0 ? (_jsxs(Stack, { direction: "row", children: [_jsx(HowlerAvatar, { userId: userIds[0], sx: { height: avatarHeight, width: avatarHeight } }), _jsx(Typography, { children: userIds[0] })] })) : showEmptyInput ? (autocomplete) : null, variant === 'compact' && (_jsx(Popover, { open: !!anchorEl, onClose: () => setAnchorEl(null), anchorEl: anchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, children: _jsx(Box, { sx: { p: 2 }, children: autocomplete }) }))] }));
50
54
  };
51
55
  export default UserList;
@@ -107,6 +107,24 @@ describe('UserList', () => {
107
107
  expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
108
108
  expect(screen.queryByRole('button')).not.toBeInTheDocument();
109
109
  });
110
+ it('renders an inline picker for an empty list variant single mode when enabled', async () => {
111
+ const user = userEvent.setup();
112
+ const onChange = vi.fn();
113
+ render(_jsx(UserList, { i18nLabel: "user.list.label", userIds: [], onChange: onChange, variant: "list", showEmptyInput: true }), {
114
+ wrapper: createWrapper(defaultUsers)
115
+ });
116
+ const combo = screen.getByRole('combobox', { name: 'user.list.label' });
117
+ await user.click(combo);
118
+ const listbox = await screen.findByRole('listbox');
119
+ await user.click(within(listbox).getByText('Bob Analyst'));
120
+ expect(onChange).toHaveBeenCalledWith(['analystB']);
121
+ });
122
+ it('does not render an empty picker in list variant single mode by default', () => {
123
+ render(_jsx(UserList, { i18nLabel: "user.list.label", userIds: [], onChange: vi.fn(), variant: "list" }), {
124
+ wrapper: createWrapper(defaultUsers)
125
+ });
126
+ expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
127
+ });
110
128
  it('does not open popover when disabled', async () => {
111
129
  const onChange = vi.fn();
112
130
  render(_jsx(UserList, { i18nLabel: "user.list.label", userIds: ['analystA'], onChange: onChange, disabled: true, multiple: true }), {
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Delete, PersonAdd } from '@mui/icons-material';
3
- import { Box, Button, Dialog, DialogContent, DialogTitle, Divider, IconButton, List, ListItem, ListItemText, Stack, TextField, ToggleButton, ToggleButtonGroup } from '@mui/material';
3
+ import { Button, Dialog, DialogContent, DialogTitle, Divider, FormControl, IconButton, InputLabel, List, ListItem, ListItemText, MenuItem, Select, Stack, TextField } from '@mui/material';
4
4
  import { useAppUser } from '@tui/core';
5
5
  import api from '@cccsaurora/howler-ui/api';
6
6
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
@@ -23,30 +23,33 @@ export const MembershipManagement = ({ type, entity, onChange, size }) => {
23
23
  const { showSuccessMessage, showWarningMessage, showErrorMessage } = useMySnackbar();
24
24
  const [selectedUserIds, setSelectedUserIds] = useState([]);
25
25
  const [memberQuery, setMemberQuery] = useState('');
26
- const [privilege, setPrivilege] = useState('members');
26
+ const [pendingMembers, setPendingMembers] = useState([]);
27
27
  const [open, setOpen] = useState(false);
28
28
  const canManageMembership = entity &&
29
29
  user &&
30
30
  (entity.owner === user.username || entity.admins?.includes(user.username) || !!user.roles?.includes('admin'));
31
31
  const members = getAllMembers(entity);
32
- const users = useMyUserList(members.map(([member]) => member));
32
+ const users = useMyUserList([...members.map(([member]) => member), ...pendingMembers.map(({ user_id }) => user_id)]);
33
+ const handleQueueMember = useCallback(([user_id]) => {
34
+ if (!user_id) {
35
+ return;
36
+ }
37
+ setPendingMembers(current => [...current, { user_id, privilege: 'members' }]);
38
+ setSelectedUserIds([]);
39
+ }, []);
33
40
  const handleAddMember = useCallback(async () => {
34
- if (selectedUserIds.length === 0) {
41
+ if (pendingMembers.length === 0) {
35
42
  showWarningMessage(t('membership.message.warning'));
36
43
  return;
37
44
  }
38
45
  try {
39
- const updatedEntity = (await dispatchApi(api[type].permission.put(params.id, {
40
- privilege,
41
- user_ids: selectedUserIds
42
- })));
46
+ const updatedEntity = (await dispatchApi(api[type].permission.put(params.id, pendingMembers)));
43
47
  if (!updatedEntity) {
44
48
  throw new Error('Updated Entity was not returned.');
45
49
  }
46
- showSuccessMessage(t('membership.message.success'));
47
50
  onChange(updatedEntity);
48
- setSelectedUserIds([]);
49
- setMemberQuery('');
51
+ showSuccessMessage(t('membership.message.success'));
52
+ setPendingMembers([]);
50
53
  }
51
54
  catch {
52
55
  showErrorMessage(t('membership.message.error'));
@@ -54,8 +57,7 @@ export const MembershipManagement = ({ type, entity, onChange, size }) => {
54
57
  }, [
55
58
  dispatchApi,
56
59
  params.id,
57
- selectedUserIds,
58
- privilege,
60
+ pendingMembers,
59
61
  t,
60
62
  showSuccessMessage,
61
63
  showErrorMessage,
@@ -66,10 +68,7 @@ export const MembershipManagement = ({ type, entity, onChange, size }) => {
66
68
  // Keep the targeted privilege explicit so we remove the intended permission entry.
67
69
  const handleRemoveMember = useCallback(async (user_id, targetPrivilege) => {
68
70
  try {
69
- const updatedEntity = (await dispatchApi(api[type].permission.delete(params.id, {
70
- privilege: targetPrivilege,
71
- user_ids: [user_id]
72
- }), {
71
+ const updatedEntity = (await dispatchApi(api[type].permission.delete(params.id, [{ privilege: targetPrivilege, user_id }]), {
73
72
  throwError: false
74
73
  }));
75
74
  if (!updatedEntity) {
@@ -85,9 +84,8 @@ export const MembershipManagement = ({ type, entity, onChange, size }) => {
85
84
  useEffect(() => {
86
85
  if (open) {
87
86
  // Reset modal state each time it opens to avoid leaking stale UI state.
88
- setSelectedUserIds([]);
89
87
  setMemberQuery('');
90
- setPrivilege('members');
88
+ setPendingMembers([]);
91
89
  }
92
90
  // oxlint-disable-next-line react-hooks/exhaustive-deps
93
91
  }, [open]);
@@ -108,12 +106,19 @@ export const MembershipManagement = ({ type, entity, onChange, size }) => {
108
106
  if (!canManageMembership) {
109
107
  return null;
110
108
  }
111
- return (_jsxs(_Fragment, { children: [_jsx(CustomButton, { sx: { flexShrink: 0 }, variant: "outlined", startIcon: _jsx(PersonAdd, {}), onClick: () => setOpen(true), size: size, children: t('membership.manage') }), _jsxs(Dialog, { open: open, onClose: () => setOpen(false), slotProps: { paper: { sx: { maxWidth: '50vw' } } }, children: [_jsx(DialogTitle, { children: t('membership.manage') }), _jsx(DialogContent, { sx: { minHeight: '300px', maxWidth: '80vw', display: 'flex', flexDirection: 'column' }, children: _jsxs(Stack, { direction: "row", divider: _jsx(Divider, { orientation: "vertical", flexItem: true }), spacing: 1, flex: 1, alignItems: "stretch", pt: 0.5, children: [_jsxs(Stack, { spacing: 1, children: [_jsx(TextField, { fullWidth: true, size: "small", label: t('search'), value: memberQuery, onChange: event => setMemberQuery(event.target.value), sx: { minWidth: '300px' } }), _jsx(List, { children: filteredMembers.map(([m, privilege]) => (_jsx(ListItem, { children: _jsxs(Stack, { direction: "row", sx: { alignItems: 'center', width: '100%' }, spacing: 1, children: [_jsx(HowlerAvatar, { userId: m || 'Unknown' }), _jsx(ListItemText, { primary: users[m]?.name || m, secondary: users[m]?.email
109
+ return (_jsxs(_Fragment, { children: [_jsx(CustomButton, { sx: { flexShrink: 0 }, variant: "outlined", startIcon: _jsx(PersonAdd, {}), onClick: () => setOpen(true), size: size, children: t('membership.manage') }), _jsxs(Dialog, { open: open, onClose: () => setOpen(false), slotProps: { paper: { sx: { maxWidth: '50vw' } } }, children: [_jsx(DialogTitle, { children: t('membership.manage') }), _jsx(DialogContent, { sx: { minHeight: '300px', minWidth: '840px', maxWidth: '80vw', display: 'flex', flexDirection: 'column' }, children: _jsxs(Stack, { direction: "row", divider: _jsx(Divider, { orientation: "vertical", flexItem: true }), spacing: 1, flex: 1, alignItems: "stretch", pt: 0.5, children: [_jsxs(Stack, { spacing: 1, flex: 1, children: [_jsx(TextField, { fullWidth: true, size: "small", label: t('search'), value: memberQuery, onChange: event => setMemberQuery(event.target.value), sx: { minWidth: '300px' } }), _jsx(List, { children: filteredMembers.map(([m, privilege]) => (_jsx(ListItem, { children: _jsxs(Stack, { direction: "row", sx: { alignItems: 'center', width: '100%' }, spacing: 1, children: [_jsx(HowlerAvatar, { userId: m || 'Unknown' }), _jsx(ListItemText, { primary: users[m]?.name || m, secondary: users[m]?.email
112
110
  ? `${getPrivilegeLabel(privilege)} - ${users[m].email}`
113
- : getPrivilegeLabel(privilege) }), _jsx(FlexOne, {}), privilege !== 'owner' && (_jsx(IconButton, { disabled: !params.id, onClick: () => handleRemoveMember(m, privilege), children: _jsx(Delete, { color: "error" }) }))] }) }, `${m}-${privilege}`))) })] }), _jsxs(Stack, { spacing: 1, children: [_jsx(ToggleButtonGroup, { exclusive: true, value: privilege, onChange: (_e, _privilege) => setPrivilege(_privilege), children: availablePrivileges.map(k => (_jsx(ToggleButton, { value: k, children: getPrivilegeLabel(k) }, k))) }), _jsx(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: _jsx(UserList, { variant: "list", multiple: true, i18nLabel: t('page.login.username'), userIds: selectedUserIds, onChange: setSelectedUserIds, except: members
114
- .filter(([_, _privilege]) => _privilege === 'owner' || privilege === _privilege)
115
- .map(([member]) => member) }) }), _jsx(Button, { onClick: handleAddMember, sx: { mt: 3 }, variant: "contained", fullWidth: true, disabled: !params.id ||
116
- selectedUserIds.length === 0 ||
117
- !privilege ||
118
- (privilege === 'owner' && selectedUserIds.length > 1), children: t('add') })] })] }) })] })] }));
111
+ : getPrivilegeLabel(privilege) }), _jsx(FlexOne, {}), privilege !== 'owner' && (_jsx(IconButton, { disabled: !params.id, onClick: () => handleRemoveMember(m, privilege), children: _jsx(Delete, { color: "error" }) }))] }) }, `${m}-${privilege}`))) })] }), _jsxs(Stack, { spacing: 1, flex: 1, children: [_jsx(UserList, { variant: "list", i18nLabel: t('page.login.username'), userIds: selectedUserIds, onChange: handleQueueMember, except: [...members.map(([member]) => member), ...pendingMembers.map(({ user_id }) => user_id)], disabled: !params.id, showEmptyInput: true }), _jsx(List, { children: pendingMembers.map(({ user_id, privilege }) => {
112
+ const roleLabelId = `pending-member-role-${user_id}`;
113
+ const hasPendingOwner = pendingMembers.some(member => member.user_id !== user_id && member.privilege === 'owner');
114
+ return (_jsxs(ListItem, { disableGutters: true, sx: {
115
+ display: 'grid',
116
+ gridTemplateColumns: 'minmax(0, 1fr) auto',
117
+ gridTemplateRows: 'auto auto',
118
+ columnGap: 1,
119
+ rowGap: 1
120
+ }, children: [_jsxs(Stack, { direction: "row", sx: { gridColumn: 1, gridRow: 1 }, children: [_jsx(HowlerAvatar, { userId: user_id || 'Unknown' }), _jsx(ListItemText, { primary: users[user_id]?.name || user_id, secondary: users[user_id]?.email, sx: { mx: 1 } })] }), _jsxs(FormControl, { size: "small", sx: { minWidth: 150, gridColumn: 1, gridRow: 2 }, children: [_jsx(InputLabel, { id: roleLabelId, children: t('membership.role') }), _jsx(Select, { labelId: roleLabelId, label: t('membership.role'), value: privilege, onChange: event => setPendingMembers(current => current.map(member => member.user_id === user_id
121
+ ? { ...member, privilege: event.target.value }
122
+ : member)), children: availablePrivileges.map(option => (_jsx(MenuItem, { value: option, disabled: option === 'owner' && hasPendingOwner, children: getPrivilegeLabel(option) }, option))) })] }), _jsx(IconButton, { "aria-label": t('button.delete'), sx: { gridColumn: 2, gridRow: 1 }, onClick: () => setPendingMembers(current => current.filter(member => member.user_id !== user_id)), children: _jsx(Delete, { color: "error" }) })] }, user_id));
123
+ }) }), _jsx(Button, { onClick: handleAddMember, sx: { mt: 3 }, variant: "contained", fullWidth: true, disabled: !params.id || pendingMembers.length === 0, children: t('button.save') })] })] }) })] })] }));
119
124
  };
@@ -323,6 +323,7 @@
323
323
  "membership.message.error": "There was an error when updating permissions.",
324
324
  "membership.message.success": "Permissions updated.",
325
325
  "membership.message.warning": "No users selected.",
326
+ "membership.role": "Role",
326
327
  "membership.privilege.admins": "Administrator",
327
328
  "membership.privilege.manage": "Manage Privileges",
328
329
  "membership.privilege.members": "Member",
@@ -323,6 +323,7 @@
323
323
  "membership.message.error": "Il y a eu une erreur durant la mise a jours des permissions",
324
324
  "membership.message.success": "Permissions misse à jours.",
325
325
  "membership.message.warning": "Aucun utilisateur sélectionné",
326
+ "membership.role": "Rôle",
326
327
  "membership.privilege.admins": "Administrateur",
327
328
  "membership.privilege.manage": "Gérer privilèges",
328
329
  "membership.privilege.members": "Membre",
package/package.json CHANGED
@@ -97,7 +97,7 @@
97
97
  "url": "https://github.com/CybercentreCanada/howler"
98
98
  },
99
99
  "type": "module",
100
- "version": "3.1.0-dev.1643",
100
+ "version": "3.1.0-dev.1647",
101
101
  "exports": {
102
102
  "./i18n": "./i18n.js",
103
103
  "./index.css": "./index.css",