Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77475f09cd | |||
| 9413cf03c9 | |||
| b2f9166c22 | |||
| 7fedfdfbfd |
@@ -1,5 +1,5 @@
|
|||||||
VITE_TOKEN_NAME = 'admin_token'
|
VITE_TOKEN_NAME = 'admin_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
||||||
VITE_BASE_URL = 'https://api.danakcorp.com'
|
# VITE_BASE_URL = 'https://api.danakcorp.com'
|
||||||
|
|
||||||
# VITE_BASE_URL = 'http://192.168.99.131:3500'
|
VITE_BASE_URL = 'http://192.168.99.131:3500'
|
||||||
@@ -5,11 +5,22 @@ type Props = {
|
|||||||
src?: string | null;
|
src?: string | null;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showTooltip?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserAvatar: FC<Props> = ({ src, alt = "", className = "size-6" }) => (
|
const UserAvatar: FC<Props> = ({ src, alt = "", className = "size-6", showTooltip = true }) => (
|
||||||
<div className={`${className} rounded-full overflow-hidden shrink-0 bg-[#D0D0D0]`}>
|
<div className={`relative group/avatar ${className} rounded-full overflow-visible shrink-0`}>
|
||||||
<img src={src || AvatarImage} alt={alt} className="size-full object-cover" />
|
<div className="size-full rounded-full overflow-hidden bg-[#D0D0D0]">
|
||||||
|
<img src={src || AvatarImage} alt={alt} className="size-full object-cover" />
|
||||||
|
</div>
|
||||||
|
{showTooltip && alt ? (
|
||||||
|
<span
|
||||||
|
role="tooltip"
|
||||||
|
className="pointer-events-none absolute bottom-full left-1/2 z-20 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md bg-[#292D32] px-2 py-1 text-[10px] text-white opacity-0 transition-opacity group-hover/avatar:opacity-100"
|
||||||
|
>
|
||||||
|
{alt}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const TaskDetailDescription: FC<Props> = ({ value = "", onChange, onSave, isSavi
|
|||||||
onChange={(e) => onChange?.(e.target.value)}
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
placeholder={t("taskmanager.task_detail.description_placeholder")}
|
placeholder={t("taskmanager.task_detail.description_placeholder")}
|
||||||
rows={5}
|
rows={5}
|
||||||
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 text-xs outline-none resize-none placeholder:text-description"
|
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 pl-20 text-xs outline-none resize-none placeholder:text-description"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
taskDetail={taskDetail}
|
taskDetail={taskDetail}
|
||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
|
projectId={taskDetail?.projectId ?? projectId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,10 @@ const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, wid
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("pointerdown", handlePointerDown);
|
// Capture: DefaulModal stops pointerdown propagation on bubble, so outside-click
|
||||||
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
// would never reach a document bubble listener while the modal is open.
|
||||||
|
document.addEventListener("pointerdown", handlePointerDown, true);
|
||||||
|
return () => document.removeEventListener("pointerdown", handlePointerDown, true);
|
||||||
}, [anchorRef, onClose]);
|
}, [anchorRef, onClose]);
|
||||||
|
|
||||||
if (!position) return null;
|
if (!position) return null;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { type FC, useEffect, useMemo, useState } from "react";
|
import { type FC, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from '../../../../components/Toast';
|
import { toast } from '../../../../components/Toast';
|
||||||
import { useGetUsersInfinite } from "../../../users/hooks/useUserData";
|
|
||||||
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
|
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
|
||||||
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
|
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
|
||||||
import { parseApiDate, toApiDate } from "./date/utils";
|
import { parseApiDate, toApiDate } from "./date/utils";
|
||||||
@@ -15,7 +14,6 @@ import type { TaskDetailTab } from "./types";
|
|||||||
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
||||||
import TaskDetailTimePopover from "./time/TaskDetailTimePopover";
|
import TaskDetailTimePopover from "./time/TaskDetailTimePopover";
|
||||||
import type { TaskUser } from "./users/types";
|
import type { TaskUser } from "./users/types";
|
||||||
import type { UserItemType } from "../../../users/types/UserTypes";
|
|
||||||
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
||||||
import {
|
import {
|
||||||
useCreateCheckListItem,
|
useCreateCheckListItem,
|
||||||
@@ -24,6 +22,8 @@ import {
|
|||||||
useDeleteTaskRemark,
|
useDeleteTaskRemark,
|
||||||
useUpdateTask,
|
useUpdateTask,
|
||||||
} from "../../task/hooks/useTaskData";
|
} from "../../task/hooks/useTaskData";
|
||||||
|
import { useGetProjectUsers } from "../../project/hooks/useProjectData";
|
||||||
|
import type { ProjectUserType } from "../../project/types/ProjectTypes";
|
||||||
import { useSingleUpload } from "../../../service/hooks/useServiceData";
|
import { useSingleUpload } from "../../../service/hooks/useServiceData";
|
||||||
import { ErrorType } from "../../../../helpers/types";
|
import { ErrorType } from "../../../../helpers/types";
|
||||||
|
|
||||||
@@ -32,9 +32,10 @@ type Props = {
|
|||||||
onTabChange: (tab: TaskDetailTab) => void;
|
onTabChange: (tab: TaskDetailTab) => void;
|
||||||
taskDetail?: TaskDetailType;
|
taskDetail?: TaskDetailType;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
|
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId, projectId }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const createTaskRemark = useCreateTaskRemark();
|
const createTaskRemark = useCreateTaskRemark();
|
||||||
const deleteTaskRemark = useDeleteTaskRemark();
|
const deleteTaskRemark = useDeleteTaskRemark();
|
||||||
@@ -44,14 +45,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
const updateTask = useUpdateTask();
|
const updateTask = useUpdateTask();
|
||||||
const isUsersOpen = activeTab === "users";
|
const isUsersOpen = activeTab === "users";
|
||||||
const [userSearch, setUserSearch] = useState("");
|
const [userSearch, setUserSearch] = useState("");
|
||||||
const [debouncedUserSearch, setDebouncedUserSearch] = useState("");
|
const { data: usersData, isPending: isUsersPending } = useGetProjectUsers(projectId, isUsersOpen);
|
||||||
const {
|
|
||||||
data: usersData,
|
|
||||||
isPending: isUsersPending,
|
|
||||||
isFetchingNextPage: isFetchingNextUsersPage,
|
|
||||||
fetchNextPage: fetchNextUsersPage,
|
|
||||||
hasNextPage: hasNextUsersPage,
|
|
||||||
} = useGetUsersInfinite(debouncedUserSearch, undefined, isUsersOpen);
|
|
||||||
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
||||||
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
||||||
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
|
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
|
||||||
@@ -59,14 +53,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
const [startDate, setStartDate] = useState<DateObject | null>(null);
|
const [startDate, setStartDate] = useState<DateObject | null>(null);
|
||||||
const [endDate, setEndDate] = useState<DateObject | null>(null);
|
const [endDate, setEndDate] = useState<DateObject | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
|
||||||
setDebouncedUserSearch(userSearch.trim());
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => window.clearTimeout(timeoutId);
|
|
||||||
}, [userSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!taskDetail) return;
|
if (!taskDetail) return;
|
||||||
|
|
||||||
@@ -91,16 +77,13 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
}, [taskDetail]);
|
}, [taskDetail]);
|
||||||
|
|
||||||
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
|
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
|
||||||
const users = useMemo((): TaskUser[] => {
|
const projectUsers = useMemo((): TaskUser[] => {
|
||||||
const apiUsers: TaskUser[] =
|
const apiUsers: TaskUser[] =
|
||||||
usersData?.pages.flatMap(
|
usersData?.data?.map((user: ProjectUserType) => ({
|
||||||
(page) =>
|
id: user.id,
|
||||||
page.data?.users?.map((user: UserItemType) => ({
|
name: `${user.firstName} ${user.lastName}`.trim(),
|
||||||
id: user.id,
|
avatar: user.profilePic,
|
||||||
name: `${user.firstName} ${user.lastName}`.trim(),
|
})) ?? [];
|
||||||
avatar: user.profilePic,
|
|
||||||
})) ?? [],
|
|
||||||
) ?? [];
|
|
||||||
const taskUsers: TaskUser[] =
|
const taskUsers: TaskUser[] =
|
||||||
taskDetail?.users?.map((user) => ({
|
taskDetail?.users?.map((user) => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -123,20 +106,21 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(userMap.values());
|
return Array.from(userMap.values());
|
||||||
}, [usersData?.pages, taskDetail?.users]);
|
}, [usersData?.data, taskDetail?.users]);
|
||||||
const listUsers = useMemo((): TaskUser[] => {
|
const listUsers = useMemo((): TaskUser[] => {
|
||||||
return (
|
const query = userSearch.trim().toLowerCase();
|
||||||
usersData?.pages.flatMap(
|
const apiUsers: TaskUser[] =
|
||||||
(page) =>
|
usersData?.data?.map((user: ProjectUserType) => ({
|
||||||
page.data?.users?.map((user: UserItemType) => ({
|
id: user.id,
|
||||||
id: user.id,
|
name: `${user.firstName} ${user.lastName}`.trim(),
|
||||||
name: `${user.firstName} ${user.lastName}`.trim(),
|
avatar: user.profilePic,
|
||||||
avatar: user.profilePic,
|
})) ?? [];
|
||||||
})) ?? [],
|
|
||||||
) ?? []
|
if (!query) return apiUsers;
|
||||||
);
|
|
||||||
}, [usersData?.pages]);
|
return apiUsers.filter((user) => user.name.toLowerCase().includes(query));
|
||||||
const selectedUsers = users.filter((user) => selectedUserIds.has(user.id));
|
}, [usersData?.data, userSearch]);
|
||||||
|
const selectedUsers = projectUsers.filter((user) => selectedUserIds.has(user.id));
|
||||||
const isLabelsOpen = activeTab === "labels";
|
const isLabelsOpen = activeTab === "labels";
|
||||||
const isChecklistOpen = activeTab === "checklist";
|
const isChecklistOpen = activeTab === "checklist";
|
||||||
const isAttachmentOpen = activeTab === "attachment";
|
const isAttachmentOpen = activeTab === "attachment";
|
||||||
@@ -382,11 +366,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
onToggle={handleUserToggle}
|
onToggle={handleUserToggle}
|
||||||
isLoading={isUsersPending}
|
isLoading={isUsersPending}
|
||||||
isSubmitting={updateTask.isPending}
|
isSubmitting={updateTask.isPending}
|
||||||
hasNextPage={hasNextUsersPage}
|
|
||||||
isFetchingNextPage={isFetchingNextUsersPage}
|
|
||||||
onLoadMore={() => {
|
|
||||||
void fetchNextUsersPage();
|
|
||||||
}}
|
|
||||||
search={userSearch}
|
search={userSearch}
|
||||||
onSearchChange={setUserSearch}
|
onSearchChange={setUserSearch}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white/25 rounded-xl p-3">
|
<div className="bg-white/25 rounded-xl p-3">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<UserAvatar src={comment.user?.profilePic} alt={authorName} className="size-7" />
|
<UserAvatar src={comment.user?.profilePic} alt={authorName} className="size-7" showTooltip={false} />
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const TaskDetailUsersList: FC<Props> = ({
|
|||||||
onToggle={() => onToggle(user.id)}
|
onToggle={() => onToggle(user.id)}
|
||||||
ariaLabel={user.name}
|
ariaLabel={user.name}
|
||||||
/>
|
/>
|
||||||
<UserAvatar src={user.avatar} alt={user.name} />
|
<UserAvatar src={user.avatar} alt={user.name} showTooltip={false} />
|
||||||
<div className="text-[10px]">{user.name}</div>
|
<div className="text-[10px]">{user.name}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ const CreateProjectSidebar: FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
selectedUsers.map((user) => (
|
selectedUsers.map((user) => (
|
||||||
<div key={user.id} className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg">
|
<div key={user.id} className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg">
|
||||||
<UserAvatar src={user.profilePic} alt={user.name} />
|
<UserAvatar src={user.profilePic} alt={user.name} showTooltip={false} />
|
||||||
<div>{user.name}</div>
|
<div>{user.name}</div>
|
||||||
<CloseCircle variant="Bold" className="size-4 cursor-pointer" color="red" onClick={() => handleRemoveUser(user.id)} />
|
<CloseCircle variant="Bold" className="size-4 cursor-pointer" color="red" onClick={() => handleRemoveUser(user.id)} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ export const useGetProject = (id: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetProjectUsers = (id: string, enabled = true) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["project-users", id],
|
||||||
|
queryFn: () => api.getProjectUsers(id),
|
||||||
|
enabled: !!id && enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreateProject = () => {
|
export const useCreateProject = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -49,6 +57,9 @@ export const useUpdateProject = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["project-detail", variables.id],
|
queryKey: ["project-detail", variables.id],
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["project-users", variables.id],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from "../../../../config/axios";
|
import axios from "../../../../config/axios";
|
||||||
import { CreateProjectType, GetProjectResponse, UpdateProjectType } from "../types/ProjectTypes";
|
import { CreateProjectType, GetProjectResponse, GetProjectUsersResponse, UpdateProjectType } from "../types/ProjectTypes";
|
||||||
|
|
||||||
export const getProjectsByWorkspace = async (workspaceId: string) => {
|
export const getProjectsByWorkspace = async (workspaceId: string) => {
|
||||||
const { data } = await axios.get(`/task-manager/projects/user/workspace/${workspaceId}`);
|
const { data } = await axios.get(`/task-manager/projects/user/workspace/${workspaceId}`);
|
||||||
@@ -16,6 +16,11 @@ export const getProject = async (id: string): Promise<GetProjectResponse> => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProjectUsers = async (id: string): Promise<GetProjectUsersResponse> => {
|
||||||
|
const { data } = await axios.get(`/task-manager/projects/${id}/users`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const createProject = async (params: CreateProjectType) => {
|
export const createProject = async (params: CreateProjectType) => {
|
||||||
const { data } = await axios.post(`/task-manager/projects`, params);
|
const { data } = await axios.post(`/task-manager/projects`, params);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -53,3 +53,7 @@ export type ProjectWithPhasesType = {
|
|||||||
export interface GetProjectResponse extends IResponse<ProjectWithPhasesType> {
|
export interface GetProjectResponse extends IResponse<ProjectWithPhasesType> {
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetProjectUsersResponse extends IResponse<ProjectUserType[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ const CreateWorkspaceSidebar: FC<Props> = ({
|
|||||||
key={user.id}
|
key={user.id}
|
||||||
className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg"
|
className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg"
|
||||||
>
|
>
|
||||||
<UserAvatar src={user.profilePic} alt={user.name} />
|
<UserAvatar src={user.profilePic} alt={user.name} showTooltip={false} />
|
||||||
<div>{user.name}</div>
|
<div>{user.name}</div>
|
||||||
<CloseCircle
|
<CloseCircle
|
||||||
variant="Bold"
|
variant="Bold"
|
||||||
|
|||||||
Reference in New Issue
Block a user