Skip to content

Commit 89be89b

Browse files
committed
chore(INTERNAL-1708): people team management redesign
1 parent fddd8b7 commit 89be89b

20 files changed

Lines changed: 395 additions & 42 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Add team member": "",
3+
"Team member name": "",
4+
"Role": "",
5+
"Cancel": "",
6+
"Create": "",
7+
"Participation rate, %": "",
8+
"Maximum value is {max}": "",
9+
"Save": "",
10+
"Team": ""
11+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* eslint-disable */
2+
// Do not edit, use generator to update
3+
import { i18n, fmt, I18nLangSet } from 'easy-typed-intl';
4+
import getLang from '../../../utils/getLang';
5+
6+
import en from './en.json';
7+
import ru from './ru.json';
8+
9+
export type I18nKey = keyof typeof en & keyof typeof ru;
10+
type I18nLang = 'en' | 'ru';
11+
12+
const keyset: I18nLangSet<I18nKey> = {};
13+
14+
keyset['en'] = en;
15+
keyset['ru'] = ru;
16+
17+
export const tr = i18n<I18nLang, I18nKey>(keyset, fmt, getLang);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Add team member": "Добавить участника команды",
3+
"Team member name": "ФИО участника",
4+
"Role": "Роль",
5+
"Cancel": "Отмена",
6+
"Create": "Создать",
7+
"Participation rate, %": "Процент участия, %",
8+
"Maximum value is {max}": "Максимальное значение {max}",
9+
"Save": "Сохранить",
10+
"Team": "Команда"
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.Form {
2+
display: grid;
3+
grid-template-columns: 3fr;
4+
gap: var(--gap-m);
5+
}
6+
7+
.ModalHeader {
8+
background-color: var(--gray-900);
9+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { ChangeEvent, useCallback } from 'react';
2+
import { Group, Role, User } from 'prisma/prisma-client';
3+
import { useForm } from 'react-hook-form';
4+
import { zodResolver } from '@hookform/resolvers/zod';
5+
import { Form } from '@taskany/bricks';
6+
import { ModalHeader, Modal, ModalContent, Text, Button, FormControlInput, ModalCross } from '@taskany/bricks/harmony';
7+
8+
import { FormControl } from '../FormControl/FormControl';
9+
import { Nullish } from '../../utils/types';
10+
import { AddUserToGroup, addUserToGroupSchema } from '../../modules/userSchemas';
11+
import { useUserMutations } from '../../modules/userHooks';
12+
import { FormActions } from '../FormActions/FormActions';
13+
import { trpc } from '../../trpc/trpcClient';
14+
import { UserSelect } from '../UserSelect/UserSelect';
15+
import { RoleSelect } from '../RoleSelect/RoleSelect';
16+
import { MembershipInfo } from '../../modules/userTypes';
17+
import { useRoleMutations } from '../../modules/roleHooks';
18+
import { GroupComboBox } from '../GroupComboBox/GroupComboBox';
19+
20+
import { tr } from './AddUserToTeamModal.i18n';
21+
import s from './AddUserToTeamModal.module.css';
22+
23+
interface BaseProps {
24+
visible: boolean;
25+
onClose: VoidFunction;
26+
type: string;
27+
}
28+
29+
interface AddUserToTeamPageModalProps extends BaseProps {
30+
type: 'user-to-team';
31+
groupId: string;
32+
}
33+
34+
interface EditUserModalProps extends BaseProps {
35+
type: 'edit';
36+
groupId: string;
37+
membership: MembershipInfo;
38+
}
39+
40+
interface AddTeamToUserPageModalProps extends BaseProps {
41+
type: 'team-to-user';
42+
userId: string;
43+
}
44+
45+
type AddUserModalProps = AddUserToTeamPageModalProps | EditUserModalProps | AddTeamToUserPageModalProps;
46+
47+
export const AddUserToTeamModal = (props: AddUserModalProps) => {
48+
const { addUserToGroup, updatePercentage } = useUserMutations();
49+
const { addToMembership, removeFromMembership } = useRoleMutations();
50+
51+
const defaultValues = {
52+
userId:
53+
(props.type === 'team-to-user' && props.userId) ||
54+
(props.type === 'edit' && props.membership.userId) ||
55+
undefined,
56+
groupId:
57+
(props.type === 'user-to-team' && props.groupId) ||
58+
(props.type === 'edit' && props.membership.id) ||
59+
undefined,
60+
roles: (props.type === 'edit' && props.membership.roles) || undefined,
61+
percentage: (props.type === 'edit' && props.membership.percentage) || undefined,
62+
};
63+
const methods = useForm<AddUserToGroup>({
64+
resolver: zodResolver(addUserToGroupSchema),
65+
defaultValues,
66+
});
67+
68+
const {
69+
reset,
70+
handleSubmit,
71+
setValue,
72+
setError,
73+
clearErrors,
74+
watch,
75+
trigger,
76+
formState: { errors },
77+
} = methods;
78+
79+
const rolesValue = watch('roles');
80+
81+
const userIdValue = watch('userId');
82+
const availableMembershipQuery = trpc.user.getAvailableMembershipPercentage.useQuery(userIdValue, {
83+
enabled: !!userIdValue,
84+
});
85+
const max = availableMembershipQuery.data ?? 100;
86+
87+
const onSubmit = handleSubmit(async (data) => {
88+
await addUserToGroup(data);
89+
reset(defaultValues);
90+
props.onClose();
91+
});
92+
93+
const onEdit = handleSubmit(async (data) => {
94+
if (props.type === 'edit' && data.percentage) {
95+
await updatePercentage({
96+
membershipId: props.membership.id,
97+
groupId: data.groupId,
98+
percentage: data.percentage,
99+
});
100+
}
101+
102+
if (props.type === 'edit' && data.roles && props.membership.roles[0]?.name !== data.roles[0].name) {
103+
if (props.membership.roles[0]) {
104+
await removeFromMembership({ membershipId: props.membership.id, roleId: props.membership.roles[0].id });
105+
}
106+
await addToMembership({ membershipId: props.membership.id, id: data.roles[0].id, type: 'existing' });
107+
}
108+
reset(defaultValues);
109+
props.onClose();
110+
});
111+
112+
const onUserChange = useCallback(
113+
(user: Nullish<User>) => {
114+
if (user) {
115+
setValue('userId', user.id);
116+
trigger('userId');
117+
} else {
118+
reset({ ...defaultValues, userId: undefined });
119+
}
120+
},
121+
[reset, setValue, trigger, defaultValues],
122+
);
123+
124+
const onRoleChange = (r: Nullish<Role>) => {
125+
r && setValue('roles', [r]);
126+
trigger('roles');
127+
};
128+
129+
const onTeamChange = (group: Nullish<Group>) => {
130+
group && setValue('groupId', group.id);
131+
trigger('groupId');
132+
};
133+
134+
const onPercentageChange = useCallback(
135+
(e: ChangeEvent<HTMLInputElement>) => {
136+
const percentage = e.target.value === '' ? undefined : parseInt(e.target.value, 10);
137+
setValue('percentage', percentage);
138+
if (percentage && percentage > max) {
139+
setError('percentage', { message: tr('Maximum value is {max}', { max }) });
140+
} else {
141+
clearErrors('percentage');
142+
}
143+
},
144+
[setValue, setError, clearErrors, max],
145+
);
146+
147+
let role;
148+
149+
if (rolesValue) {
150+
role = rolesValue[0] ? rolesValue[0].name : undefined;
151+
}
152+
153+
return (
154+
<Modal visible={props.visible} onClose={props.onClose} width={530}>
155+
<ModalHeader className={s.ModalHeader}>
156+
<Text weight="bold">{tr('Add team member')}</Text>
157+
<ModalCross onClick={props.onClose} />
158+
</ModalHeader>
159+
160+
<ModalContent>
161+
<Form onSubmit={props.type === 'edit' ? onEdit : onSubmit} className={s.Form}>
162+
<FormControl label={props.type === 'team-to-user' ? tr('Team') : tr('Team member name')} required>
163+
{props.type === 'team-to-user' ? (
164+
<GroupComboBox
165+
defaultGroupId={watch('groupId')}
166+
onChange={onTeamChange}
167+
error={errors.groupId}
168+
/>
169+
) : (
170+
<UserSelect
171+
readOnly={props.type === 'edit'}
172+
mode="single"
173+
selectedUsers={userIdValue ? [userIdValue] : undefined}
174+
onChange={(users) => onUserChange(users[0])}
175+
error={errors.userId}
176+
/>
177+
)}
178+
</FormControl>
179+
<FormControl label={tr('Role')} required>
180+
<RoleSelect onChange={onRoleChange} roleName={role} error={errors.roles} />
181+
</FormControl>
182+
<FormControl label={tr('Participation rate, %')} error={errors.percentage}>
183+
<FormControlInput
184+
placeholder={tr('Participation rate, %')}
185+
outline
186+
size="m"
187+
autoComplete="off"
188+
type="number"
189+
step={1}
190+
onChange={onPercentageChange}
191+
value={watch('percentage') || undefined}
192+
/>
193+
</FormControl>
194+
195+
<FormActions>
196+
<Button type="button" text={tr('Cancel')} onClick={props.onClose} />
197+
<Button
198+
type="submit"
199+
text={props.type === 'edit' ? tr('Save') : tr('Create')}
200+
view="primary"
201+
size="m"
202+
/>
203+
</FormActions>
204+
</Form>
205+
</ModalContent>
206+
</Modal>
207+
);
208+
};
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"Edit": "",
3-
"Remove": ""
3+
"Remove": "",
4+
"Do you really want to remove a member {user} from the team {team}": ""
45
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"Edit": "Редактировать",
3-
"Remove": "Удалить"
3+
"Remove": "Удалить",
4+
"Do you really want to remove a member {user} from the team {team}": "Вы действительно хотите удалить участника {user} из команды {team}"
45
}

src/components/MembershipEditMenu/MembershipEditMenu.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { Dropdown, MenuItem } from '@taskany/bricks';
33
import { IconMoreVerticalOutline } from '@taskany/icons';
44

55
import { MembershipInfo } from '../../modules/userTypes';
6-
import { EditRolesModal } from '../EditRolesModal/EditRolesModal';
7-
import { RemoveUserFromGroupModal } from '../RemoveUserFromGroupModal/RemoveUserFromGroupModal';
6+
import { AddUserToTeamModal } from '../AddUserToTeamModal/AddUserToTeamModal';
7+
import { WarningModal } from '../WarningModal/WarningModal';
8+
import { useUserMutations } from '../../modules/userHooks';
89

910
import { tr } from './MembershipEditMenu.i18n';
1011

@@ -16,6 +17,8 @@ export const MembershipEditMenu = ({ membership }: MembershipEditMenuProps) => {
1617
const [editRolesModalVisible, setEditRolesModalVisible] = useState(false);
1718
const [removeModalVisible, setRemoveModalVisible] = useState(false);
1819

20+
const { removeUserFromGroup } = useUserMutations();
21+
1922
const items = useMemo(
2023
() => [
2124
{ name: tr('Edit'), action: () => setEditRolesModalVisible(true) },
@@ -24,6 +27,11 @@ export const MembershipEditMenu = ({ membership }: MembershipEditMenuProps) => {
2427
[],
2528
);
2629

30+
const onRemoveClick = async (membership: MembershipInfo) => {
31+
await removeUserFromGroup({ userId: membership.userId, groupId: membership.groupId });
32+
setRemoveModalVisible(false);
33+
};
34+
2735
return (
2836
<>
2937
<Dropdown
@@ -37,16 +45,23 @@ export const MembershipEditMenu = ({ membership }: MembershipEditMenuProps) => {
3745
)}
3846
/>
3947

40-
<EditRolesModal
41-
visible={editRolesModalVisible}
48+
<AddUserToTeamModal
4249
membership={membership}
50+
visible={editRolesModalVisible}
4351
onClose={() => setEditRolesModalVisible(false)}
52+
groupId={membership.groupId}
53+
type="edit"
4454
/>
4555

46-
<RemoveUserFromGroupModal
56+
<WarningModal
57+
view="danger"
58+
warningText={tr('Do you really want to remove a member {user} from the team {team}', {
59+
user: membership.user.name || 'user',
60+
team: membership.group.name,
61+
})}
4762
visible={removeModalVisible}
48-
membership={membership}
49-
onClose={() => setRemoveModalVisible(false)}
63+
onCancel={() => setRemoveModalVisible(false)}
64+
onConfirm={() => onRemoveClick(membership)}
5065
/>
5166
</>
5267
);

src/components/TeamMembers/TeamMembers.i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"Add": "",
44
"Edit": "",
55
"Delete": "",
6-
"No members": ""
6+
"No members": "",
7+
"Do you really want to remove a member {user} from the team {team}": ""
78
}

src/components/TeamMembers/TeamMembers.i18n/ru.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"Add": "Добавить",
44
"Edit": "Редактировать",
55
"Delete": "Удалить",
6-
"No members": "Нет участников"
6+
"No members": "Нет участников",
7+
"Do you really want to remove a member {user} from the team {team}": "Вы действительно хотите удалить участника {user} из команды {team}"
78
}

0 commit comments

Comments
 (0)