Skip to content

Commit b6169bc

Browse files
authored
[FixBug] Infinite loop: assignTask + log functions firing repeatedly when Assign Task popup is open (Profile Page | Unassigned Tab) (#3998)
* fix infinite re-render / fix multiple member assignement * add spinner when updating / disable update button * add ai bot suggestions
1 parent b88b171 commit b6169bc

File tree

4 files changed

+57
-63
lines changed

4 files changed

+57
-63
lines changed

apps/web/core/components/common/image-overlapper.tsx

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { useCallback, useState } from 'react';
1+
import { useCallback, useMemo, useState } from 'react';
22
import { Popover, PopoverContent, PopoverTrigger } from '@/core/components/common/popover';
33
import Image from 'next/image';
44
import Link from 'next/link';
55
import Skeleton from 'react-loading-skeleton';
66
import { ScrollArea } from '@/core/components/common/scroll-bar';
7-
import { useModal } from '@/core/hooks';
8-
import { Modal, Divider } from '@/core/components';
7+
import { useModal, useTeamTasks } from '@/core/hooks';
8+
import { Modal, Divider, SpinnerLoader } from '@/core/components';
99
import { useTranslations } from 'next-intl';
1010
import { TaskAssignButton } from '@/core/components/tasks/task-assign-button';
1111
import { clsxm } from '@/core/lib/utils';
@@ -19,6 +19,7 @@ import { ITimerStatus } from '@/core/types/interfaces/timer/timer-status';
1919
import { TEmployee, TTask } from '@/core/types/schemas/task/task.schema';
2020
import { useAtomValue } from 'jotai';
2121
import { activeTeamState } from '@/core/stores';
22+
import { TOrganizationTeamEmployee } from '@/core/types/schemas';
2223

2324
export interface ImageOverlapperProps {
2425
id: string;
@@ -67,26 +68,29 @@ export default function ImageOverlapper({
6768
const { isOpen, openModal, closeModal } = useModal();
6869

6970
const activeTeam = useAtomValue(activeTeamState);
70-
const allMembers = activeTeam?.members || [];
71+
const allMembers = useMemo(() => activeTeam?.members || [], [activeTeam]);
7172
const [assignedMembers, setAssignedMembers] = useState<TEmployee[]>([...(item?.members || [])]);
7273
const [unassignedMembers, setUnassignedMembers] = useState<TEmployee[]>([]);
73-
const [validate, setValidate] = useState<boolean>(false);
7474
const [showInfo, setShowInfo] = useState<boolean>(false);
75+
const { updateTask, updateLoading } = useTeamTasks();
7576

7677
const t = useTranslations();
7778

78-
const onCheckMember = (member: any) => {
79-
const checkUser = assignedMembers.some((el: TEmployee) => el.id === member.id);
80-
if (checkUser) {
81-
const updatedMembers = assignedMembers.filter((el: TEmployee) => el.id != member.id);
82-
setAssignedMembers(updatedMembers);
83-
setUnassignedMembers([...unassignedMembers, member]);
84-
} else {
85-
setAssignedMembers([...assignedMembers, member]);
86-
const updatedUnassign = unassignedMembers.filter((el: TEmployee) => el.id != member.id);
87-
setUnassignedMembers(updatedUnassign);
88-
}
89-
};
79+
const handleMemberClick = useCallback(
80+
(member: TEmployee) => {
81+
const checkUser = assignedMembers.some((el: TEmployee) => el.id === member.id);
82+
if (checkUser) {
83+
const updatedMembers = assignedMembers.filter((el: TEmployee) => el.id !== member.id);
84+
setAssignedMembers(updatedMembers);
85+
setUnassignedMembers([...unassignedMembers, member]);
86+
} else {
87+
setAssignedMembers([...assignedMembers, member]);
88+
const updatedUnassign = unassignedMembers.filter((el: TEmployee) => el.id !== member.id);
89+
setUnassignedMembers(updatedUnassign);
90+
}
91+
},
92+
[assignedMembers, unassignedMembers]
93+
);
9094

9195
const onRedirect = useCallback(
9296
(image: ImageOverlapperProps): Url => {
@@ -109,15 +113,23 @@ export default function ImageOverlapper({
109113
[activeTeam?.members, onAvatarClickRedirectTo]
110114
);
111115

112-
const onCLickValidate = () => {
113-
setValidate(!validate);
114-
closeModal();
115-
};
116+
const onConfirm = useCallback(async () => {
117+
if (updateLoading) return;
118+
try {
119+
await updateTask({
120+
...item,
121+
members: assignedMembers
122+
});
123+
124+
closeModal();
125+
} catch (error) {
126+
console.error('Error updating task members:', error);
127+
}
128+
}, [closeModal, updateTask, item, assignedMembers, updateLoading]);
116129

117130
const hasMembers = item?.members?.length > 0;
118-
const membersList = { assignedMembers, unassignedMembers };
119131

120-
if (imageLength == undefined) {
132+
if (imageLength === undefined) {
121133
return <Skeleton height={40} width={40} borderRadius={100} className="rounded-full dark:bg-[#353741]" />;
122134
}
123135

@@ -185,18 +197,16 @@ export default function ImageOverlapper({
185197
>
186198
<Divider className="mt-4" />
187199
<ul className="overflow-auto p-5 py-6">
188-
{allMembers?.map((member: any) => {
200+
{allMembers?.map((member: TOrganizationTeamEmployee) => {
189201
return (
190202
<li
191-
key={member.employee}
203+
key={member.id}
192204
className="rounded-lg border border-transparent cursor-pointer w-100 hover:border-blue-500 hover:border-opacity-50"
193205
>
194206
<TeamMember
195207
member={member}
196-
item={item}
197-
onCheckMember={onCheckMember}
198-
membersList={membersList}
199-
validate={validate}
208+
onClick={handleMemberClick}
209+
assigned={assignedMembers.some((el) => el.id === member.employee?.id)}
200210
/>
201211
</li>
202212
);
@@ -209,12 +219,13 @@ export default function ImageOverlapper({
209219
<TaskAvatars task={{ members: assignedMembers }} limit={3} />
210220
<div className="flex px-4 h-fit">
211221
<button
212-
className="flex flex-row gap-1 justify-center items-center px-4 py-2 w-28 min-w-0 h-12 text-sm text-white rounded-xl bg-primary dark:bg-primary-light disabled:bg-primary-light disabled:opacity-40"
222+
className="flex flex-row gap-3 justify-center items-center px-2 py-2 w-28 min-w-0 h-12 text-sm text-white rounded-xl bg-primary dark:bg-primary-light disabled:bg-primary-light disabled:opacity-40"
223+
disabled={updateLoading}
213224
onClick={() => {
214-
onCLickValidate();
225+
onConfirm();
215226
}}
216227
>
217-
<IconsCheck fill="#ffffff" />
228+
{updateLoading ? <SpinnerLoader size={20} /> : <IconsCheck fill="#ffffff" />}
218229
{t('common.CONFIRM')}
219230
</button>
220231
</div>
Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,33 @@
11
import { useTeamMemberCard } from '@/core/hooks';
2-
import { useEffect } from 'react';
3-
import { IEmployee } from '@/core/types/interfaces/organization/employee';
2+
import { useCallback } from 'react';
43
import { IconsCheck } from '@/core/components/icons';
54
import { UserInfo } from '../pages/teams/team/team-members-views/user-team-card/user-info';
5+
import { TEmployee, TOrganizationTeamEmployee } from '@/core/types/schemas';
66

77
export default function TeamMember({
88
member,
9-
item,
10-
onCheckMember,
11-
membersList,
12-
validate
9+
onClick,
10+
assigned
1311
}: {
14-
member: any;
15-
item: any;
16-
onCheckMember: any;
17-
membersList: any;
18-
validate: any;
12+
member: TOrganizationTeamEmployee;
13+
onClick: (member: TEmployee) => void;
14+
assigned: boolean;
1915
}) {
2016
const memberInfo = useTeamMemberCard(member);
21-
const { assignTask } = useTeamMemberCard(member);
22-
const checkAssign = membersList.assignedMembers.some((el: IEmployee) => el.id === member.employeeId);
23-
const checkUnassign = membersList.unassignedMembers.some((el: IEmployee) => el.id === member.employeeId);
2417

25-
useEffect(() => {
26-
if (validate) {
27-
if (checkAssign) {
28-
assignTask(item);
29-
} else if (checkUnassign) {
30-
memberInfo.unassignTask(item);
31-
}
32-
}
33-
}, [validate, checkAssign, checkUnassign, item, assignTask, memberInfo]);
34-
35-
const assignMember = () => {
36-
onCheckMember(member.employee);
37-
};
18+
const toggleMember = useCallback(() => {
19+
onClick(member.employee);
20+
}, [onClick, member]);
3821

3922
return (
4023
<div
4124
onClick={() => {
42-
assignMember();
25+
toggleMember();
4326
}}
4427
className="w-100 cursor-pointer flex items-center"
4528
>
4629
<UserInfo memberInfo={memberInfo} className="2xl:w-[20.625rem] w-100 pointer-events-none" />
47-
{checkAssign ? <IconsCheck fill="#3826a6" /> : <></>}
30+
{assigned ? <IconsCheck fill="#3826a6" /> : <></>}
4831
</div>
4932
);
5033
}

apps/web/core/hooks/organizations/teams/use-team-member-card.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export function useTeamMemberCard(member: TOrganizationTeamEmployee | undefined)
199199
setAssignTaskLoading(true);
200200
return updateTask({
201201
...task,
202-
members: [...(task.members || []), (member ? member.employee : {}) as any]
202+
members: [...(task.members || []), (member ? member : {}) as any]
203203
}).then(() => {
204204
if (isAuthUser && !activeTeamTask) {
205205
setActiveTask(task);

apps/web/core/types/schemas/organization/employee.schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const employeeSchema = z
8383
id: uuIdSchema,
8484
fullName: z.string(),
8585
email: z.string(),
86-
phone: z.string(),
86+
phone: z.string().nullable().optional(),
8787
avatar: z.string().nullable().optional()
8888
})
8989
.passthrough()

0 commit comments

Comments
 (0)