Skip to content

Commit 79db870

Browse files
committed
re-enable invites both in vlab, project, in creation and within
1 parent d4c7ce5 commit 79db870

File tree

20 files changed

+607
-200
lines changed

20 files changed

+607
-200
lines changed

__tests__/virtual-lab/VirtualLabMemberIcon.spec.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { render } from '@testing-library/react';
2+
import { render, screen } from '@testing-library/react';
33
import { Role } from '@/types/virtual-lab/members';
44
import VirtualLabMemberIcon from '@/components/VirtualLab/VirtualLabMemberIcon';
55
import colorDictionary from '@/components/VirtualLab/VirtualLabMemberIcon/availableColors';
@@ -11,6 +11,7 @@ describe('VirtualLabMemberIcon', () => {
1111
memberRole: 'member' as Role,
1212
firstName: 'John',
1313
lastName: 'Doe',
14+
inviteAccepted: true,
1415
};
1516

1617
it('should render initials based on the first name and last name', () => {
@@ -25,8 +26,8 @@ describe('VirtualLabMemberIcon', () => {
2526
// Calculate the expected index based on the code point
2627
const expectedIndex = firstChar ? firstChar % colorDictionary.length : 0;
2728

28-
const divElement = container.querySelector('div');
29-
const spanElement = container.querySelector('span');
29+
const divElement = screen.getByTestId('virtual-lab-member-icon');
30+
const spanElement = screen.getByTestId('virtual-lab-member-initials');
3031

3132
expect(divElement).toHaveStyle(
3233
`background-color: ${colorDictionary[expectedIndex].background}`
@@ -49,15 +50,15 @@ describe('VirtualLabMemberIcon', () => {
4950
});
5051

5152
it('should use the correct color dictionary index for a different first name', () => {
52-
const props = { ...defaultProps, firstName: 'Alice' };
53+
const props = { ...defaultProps, firstName: 'Alice', inviteAccepted: true };
5354
const { container } = render(<VirtualLabMemberIcon {...props} />);
5455

5556
const firstChar = 'Alice'.codePointAt(0);
5657
// Calculate the expected index based on the code point
5758
const expectedIndex = firstChar ? firstChar % colorDictionary.length : 0;
5859

59-
const divElement = container.querySelector('div');
60-
const spanElement = container.querySelector('span');
60+
const divElement = screen.getByTestId('virtual-lab-member-icon');
61+
const spanElement = screen.getByTestId('virtual-lab-member-initials');
6162

6263
expect(divElement).toHaveStyle(
6364
`background-color: ${colorDictionary[expectedIndex].background}`
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
'use client';
22

3-
import VirtualLabTeamTable from '@/components/VirtualLab/VirtualLabTeamTable';
3+
import ProjectTeamTable from '@/components/VirtualLab/ProjectTeamTable';
44
import withVirtualLabUsers from '@/components/VirtualLab/data/WithVirtualLabUsers';
55
import { ServerSideComponentProp } from '@/types/common';
66

77
export default function VirtualLabProjectTeamPage({
88
params,
99
}: ServerSideComponentProp<{ virtualLabId: string; projectId: string }>) {
1010
const { virtualLabId, projectId } = params;
11-
const WithVirtualLabProjectUsers = withVirtualLabUsers(
12-
VirtualLabTeamTable,
13-
virtualLabId,
14-
projectId
15-
);
11+
const WithVirtualLabProjectUsers = withVirtualLabUsers(ProjectTeamTable, virtualLabId, projectId);
1612
return <WithVirtualLabProjectUsers />;
1713
}

src/components/VirtualLab/CreateVirtualLabButton/MembersForm.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ function NewMemberForm({
4949
{ email: values.email, role: values.role },
5050
],
5151
}));
52+
form.resetFields();
5253
};
5354

5455
return (
5556
<Form
5657
form={form}
5758
// temporarily disabling this form for SfN. Invitation should be back afterwards
5859
// this prop disables it for all nested form elements
59-
disabled
6060
className="my-5"
6161
name="member_form"
6262
onFinish={onFinish}
@@ -79,7 +79,7 @@ function NewMemberForm({
7979
<Form.Item
8080
name="email"
8181
className="mb-0"
82-
label="Invitation to:"
82+
label="Invitation to: "
8383
rules={[
8484
{
8585
type: 'email',
@@ -88,7 +88,10 @@ function NewMemberForm({
8888
},
8989
]}
9090
>
91-
<Input className="border-transparent" placeholder="Enter email address" />
91+
<Input
92+
className="border-transparent outline-none"
93+
placeholder="Enter email address"
94+
/>
9295
</Form.Item>
9396
</div>
9497
<Form.Item label="As:" name="role" className="mb-0 flex items-center">
@@ -112,11 +115,12 @@ function NewMemberForm({
112115
);
113116
}
114117

115-
function NonInvitedMember({ label }: { label: string }) {
118+
function NonInvitedMember({ label, inviteAccepted }: { label: string; inviteAccepted?: boolean }) {
116119
return (
117120
<div className="flex flex-row items-center justify-between gap-4">
118121
<div className="flex flex-row items-center gap-4">
119122
<VirtualLabMemberIcon
123+
inviteAccepted={inviteAccepted}
120124
firstName={label.split(' ')[0]}
121125
lastName={label.split(' ')[1]}
122126
memberRole="admin"
@@ -194,7 +198,7 @@ export default function MembersForm({
194198
}}
195199
>
196200
<div className="my-10 flex w-full flex-col gap-4 text-primary-8">
197-
{data?.user.name && <NonInvitedMember label={data.user.name} />}
201+
{data?.user.name && <NonInvitedMember label={data.user.name} inviteAccepted />}
198202
{currentVirtualLab.include_members?.map((member) => (
199203
<InvitedMember
200204
key={member.email}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { PlusOutlined } from '@ant-design/icons';
5+
import { ConfigProvider, Table } from 'antd';
6+
import sortBy from 'lodash/sortBy';
7+
import find from 'lodash/find';
8+
import get from 'lodash/get';
9+
10+
import VirtualLabMemberIcon from '../VirtualLabMemberIcon';
11+
import { ModalInviteProjectMember } from '../projects/ModalInviteProjectMember';
12+
import { MockRole, Role, VirtualLabMember } from '@/types/virtual-lab/members';
13+
14+
type Props = {
15+
users: VirtualLabMember[];
16+
};
17+
18+
export default function ProjectTeamTable({ users }: Props) {
19+
const [openInviteProjectMemberModal, setOpenInviteProjectMemberModal] = useState(false);
20+
const roleOptions: { value: Role; label: string }[] = [
21+
{ value: 'admin', label: 'Administrator' },
22+
{ value: 'member', label: 'Member' },
23+
];
24+
25+
const columns = [
26+
{
27+
title: 'name',
28+
dataIndex: 'name',
29+
key: 'name',
30+
render: (_: string, record: VirtualLabMember) => (
31+
<div>
32+
<span>
33+
<VirtualLabMemberIcon
34+
inviteAccepted={record.invite_accepted}
35+
email={record.email}
36+
firstName={record.first_name}
37+
lastName={record.last_name}
38+
memberRole={record.role}
39+
/>
40+
</span>
41+
{record.invite_accepted ? (
42+
<span className="ml-4 inline-block font-bold">{`${record.first_name} ${record.last_name}`}</span>
43+
) : (
44+
<span className="ml-4 inline-block font-bold">{`${record.email}`}</span>
45+
)}
46+
</div>
47+
),
48+
},
49+
{
50+
title: 'Last active',
51+
dataIndex: 'last_active',
52+
key: 'last_active',
53+
render: () => <span className="text-primary-3" />, // Empty element for now, to be included when 'active' info is available
54+
},
55+
{
56+
title: 'Action',
57+
key: 'role',
58+
dataIndex: 'role',
59+
align: 'left',
60+
width: '200px',
61+
render: (role: MockRole) => (
62+
// <ConfigProvider
63+
// theme={{
64+
// components: {
65+
// Select: {
66+
// colorBgContainer: '#002766',
67+
// colorBgElevated: '#002766',
68+
// colorBorder: 'rgba(255, 255, 255, 0)',
69+
// colorText: 'rgb(255, 255, 255)',
70+
// optionSelectedBg: '#002766',
71+
// },
72+
// },
73+
// }}
74+
// >
75+
// <Select
76+
// suffixIcon={<DownOutlined style={{ color: 'white' }} />}
77+
// defaultValue={role}
78+
// style={{ width: 200, marginLeft: 300, float: 'right' }}
79+
// onChange={() => {}}
80+
// options={roleOptions}
81+
// />
82+
// </ConfigProvider>
83+
<div className="ml-auto text-base text-white">
84+
{get(find(roleOptions, { value: role }), 'label', '')}
85+
</div>
86+
),
87+
},
88+
];
89+
90+
return (
91+
<div className="my-10">
92+
<div className="flex items-center justify-between">
93+
<div className="flex gap-2">
94+
<span>Total members</span>
95+
<span className="font-bold">{users.length}</span>
96+
</div>
97+
<button
98+
type="button"
99+
className="flex w-[220px] justify-between border border-primary-7 bg-neutral-3 p-3"
100+
onClick={() => setOpenInviteProjectMemberModal(true)}
101+
>
102+
<span className="font-bold">Invite member</span>
103+
<PlusOutlined />
104+
</button>
105+
</div>
106+
<ConfigProvider
107+
theme={{
108+
components: {
109+
Table: {
110+
colorBgContainer: 'rgba(255, 255, 255, 0)',
111+
colorText: '#FFFFFF',
112+
borderColor: 'rgba(255, 255, 255, 0)',
113+
cellPaddingInline: 0,
114+
},
115+
},
116+
}}
117+
>
118+
<Table
119+
bordered={false}
120+
dataSource={sortBy(users, ['role'])}
121+
pagination={false}
122+
columns={columns}
123+
showHeader={false}
124+
/>
125+
</ConfigProvider>
126+
<ModalInviteProjectMember
127+
open={openInviteProjectMemberModal}
128+
onChange={() => setOpenInviteProjectMemberModal(false)}
129+
/>
130+
</div>
131+
);
132+
}

src/components/VirtualLab/VirtualLabHomePage/Member.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,28 @@ type Props = {
66
firstName: string;
77
lastName: string;
88
memberRole: Role;
9+
inviteAccepted?: boolean;
10+
email?: string;
911
};
1012

11-
export default function Member({ name, firstName, lastName, memberRole }: Props) {
13+
export default function Member({
14+
name,
15+
firstName,
16+
lastName,
17+
memberRole,
18+
inviteAccepted,
19+
email,
20+
}: Props) {
1221
return (
13-
<div className="flex max-w-[72px] grow flex-col gap-2 text-center">
14-
<VirtualLabMemberIcon firstName={firstName} lastName={lastName} memberRole={memberRole} />
15-
<div className="font-bold">{name}</div>
22+
<div className="flex flex-col items-center gap-2 p-2 text-center">
23+
<VirtualLabMemberIcon
24+
firstName={firstName}
25+
lastName={lastName}
26+
memberRole={memberRole}
27+
inviteAccepted={inviteAccepted}
28+
email={email}
29+
/>
30+
<div className="text-nowrap font-bold">{inviteAccepted ? name : email}</div>
1631
{/* Commenting out since feature is not present yet */}
1732
{/* <div className="text-primary-3">Active {lastActive}</div> */}
1833
</div>

src/components/VirtualLab/VirtualLabHomePage/VirtualLabUsers.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getVirtualLabUsers } from '@/services/virtual-lab/labs';
33

44
export default async function VirtualLabUsers({ id }: { id?: string }) {
55
const virtualLabUsers = id ? (await getVirtualLabUsers(id)).data.users : undefined;
6+
67
return (
78
<div className="w-full">
89
<div className="my-5 text-lg font-bold uppercase">Members</div>
@@ -11,6 +12,8 @@ export default async function VirtualLabUsers({ id }: { id?: string }) {
1112
{virtualLabUsers?.map((user) => (
1213
<div key={user.id} className="mr-20">
1314
<Member
15+
inviteAccepted={user.invite_accepted}
16+
email={user.email}
1417
name={user.name}
1518
memberRole={user.role}
1619
firstName={user.first_name}

src/components/VirtualLab/VirtualLabMemberIcon/index.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import { useMemo } from 'react';
2+
import { CheckCircleFilled, ClockCircleFilled } from '@ant-design/icons';
23

34
import colorDictionary from './availableColors';
45
import { Role } from '@/types/virtual-lab/members';
6+
import { classNames } from '@/util/utils';
57

68
type Props = {
79
memberRole: Role;
810
firstName: string;
911
lastName: string;
12+
inviteAccepted?: boolean;
13+
email?: string;
1014
};
1115

12-
export default function VirtualLabMemberIcon({ memberRole, firstName, lastName }: Props) {
16+
export default function VirtualLabMemberIcon({
17+
memberRole,
18+
firstName,
19+
lastName,
20+
inviteAccepted,
21+
email,
22+
}: Props) {
1323
const initials = useMemo(() => {
14-
return `${firstName[0]}${lastName[0]}`;
15-
}, [firstName, lastName]);
24+
return inviteAccepted ? `${firstName[0]}${lastName[0]}` : email?.split('@')[0].slice(0, 2);
25+
}, [firstName, lastName, inviteAccepted, email]);
1626

1727
const index = useMemo(() => {
1828
const codePoint = firstName.codePointAt(0);
@@ -26,10 +36,33 @@ export default function VirtualLabMemberIcon({ memberRole, firstName, lastName }
2636
return (
2737
<div
2838
style={{ backgroundColor: colorDictionary[index].background }}
29-
className={`inline-flex h-[72px] w-[72px] items-center justify-center ${memberRole === 'member' ? 'rounded-full' : ''}`}
39+
className={`relative inline-flex h-[72px] w-[72px] items-center justify-center ${memberRole === 'member' ? 'rounded-full' : ''}`}
3040
data-testid="virtual-lab-member-icon"
3141
>
32-
<span className="text-2xl font-bold" style={{ color: colorDictionary[index].color }}>
42+
{inviteAccepted ? (
43+
<div
44+
className={classNames(
45+
'absolute',
46+
memberRole === 'admin' ? 'right-0 top-0' : '-top-px right-[3px]'
47+
)}
48+
>
49+
<CheckCircleFilled className="p-1" style={{ mixBlendMode: 'difference' }} />
50+
</div>
51+
) : (
52+
<div
53+
className={classNames(
54+
'absolute',
55+
memberRole === 'admin' ? 'right-0 top-0' : '-top-px right-[3px]'
56+
)}
57+
>
58+
<ClockCircleFilled className="p-1" style={{ mixBlendMode: 'difference' }} />
59+
</div>
60+
)}
61+
<span
62+
className="text-2xl font-bold uppercase"
63+
style={{ color: colorDictionary[index].color }}
64+
data-testid="virtual-lab-member-initials"
65+
>
3366
{initials}
3467
</span>
3568
</div>

0 commit comments

Comments
 (0)