@cccsaurora/howler-ui 3.1.0-dev.1643 → 3.1.0-dev.1648
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.
- package/api/utils/createPermissionsApi.d.ts +3 -2
- package/components/elements/MembershipManagement.test.js +27 -16
- package/components/elements/UserList.d.ts +1 -0
- package/components/elements/UserList.js +7 -3
- package/components/elements/UserList.test.js +18 -0
- package/components/elements/membership/MembershipManagement.js +31 -26
- package/locales/en/translation.json +1 -0
- package/locales/fr/translation.json +1 -0
- package/package.json +3 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type PermissionEntry = {
|
|
2
2
|
privilege: string;
|
|
3
|
-
|
|
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('
|
|
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: '
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
});
|
|
@@ -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(() =>
|
|
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 {
|
|
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 [
|
|
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 (
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@jsonforms/core": "^3.8.0",
|
|
25
25
|
"@jsonforms/material-renderers": "^3.8.0",
|
|
26
26
|
"@jsonforms/react": "^3.8.0",
|
|
27
|
-
"@microlink/react-json-view": "^1.31.
|
|
27
|
+
"@microlink/react-json-view": "^1.31.36",
|
|
28
28
|
"@monaco-editor/react": "^4.7.0",
|
|
29
29
|
"@tui/a11y": "file:./tui/a11y-4.0.10.tgz",
|
|
30
30
|
"@tui/apps": "file:./tui/apps-4.0.10.tgz",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"react-markdown": "^10.1.0",
|
|
61
61
|
"react-pluggable": "^0.4.3",
|
|
62
62
|
"react-resize-detector": "^9.1.1",
|
|
63
|
-
"react-router": "7.18.
|
|
63
|
+
"react-router": "7.18.4",
|
|
64
64
|
"react-syntax-highlighter": "^15.6.6",
|
|
65
65
|
"rehype-raw": "^7.0.0",
|
|
66
66
|
"remark": "^15.0.1",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"url": "https://github.com/CybercentreCanada/howler"
|
|
98
98
|
},
|
|
99
99
|
"type": "module",
|
|
100
|
-
"version": "3.1.0-dev.
|
|
100
|
+
"version": "3.1.0-dev.1648",
|
|
101
101
|
"exports": {
|
|
102
102
|
"./i18n": "./i18n.js",
|
|
103
103
|
"./index.css": "./index.css",
|