This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { FC, Fragment, ReactNode, useEffect } from 'react'
|
import { FC, Fragment, ReactNode, useEffect } from 'react'
|
||||||
|
import { clx } from '../helpers/utils'
|
||||||
import HeaderModal from './HeaderModal'
|
import HeaderModal from './HeaderModal'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -8,6 +9,8 @@ interface Props {
|
|||||||
isHeader?: boolean,
|
isHeader?: boolean,
|
||||||
title_header?: string,
|
title_header?: string,
|
||||||
width?: number
|
width?: number
|
||||||
|
/** Raise above popovers (z-80) when confirm opens from nested panels */
|
||||||
|
elevated?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaulModal: FC<Props> = (props: Props) => {
|
const DefaulModal: FC<Props> = (props: Props) => {
|
||||||
@@ -26,7 +29,14 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
|||||||
{
|
{
|
||||||
props.open && (
|
props.open && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-hidden fixed inset-0 z-[60] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
<div
|
||||||
|
style={{ maxWidth: props.width }}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
className={clx(
|
||||||
|
'xl:justify-center xl:items-center items-end flex overflow-hidden fixed inset-0 h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto',
|
||||||
|
props.elevated ? 'z-[100]' : 'z-[60]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className='relative max-h-[85vh] bottom-0 left-0 flex xl:items-center items-end w-full xl:my-6 xl:p-2'>
|
<div className='relative max-h-[85vh] bottom-0 left-0 flex xl:items-center items-end w-full xl:my-6 xl:p-2'>
|
||||||
<div className='border-0 max-h-[85vh] p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full modalGlass2 outline-none focus:outline-none'>
|
<div className='border-0 max-h-[85vh] p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full modalGlass2 outline-none focus:outline-none'>
|
||||||
|
|
||||||
@@ -46,7 +56,14 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 modalGlass inset-0 z-50 '></div>
|
<div
|
||||||
|
onClick={props.close}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
className={clx(
|
||||||
|
'fixed size-full top-0 bottom-0 right-0 modalGlass inset-0',
|
||||||
|
props.elevated ? 'z-[90]' : 'z-50',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import DefaulModal from './DefaulModal'
|
import { createPortal } from 'react-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
|
import DefaulModal from './DefaulModal'
|
||||||
import Textarea from './Textarea'
|
import Textarea from './Textarea'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -18,12 +19,13 @@ const ModalConfrim: FC<Props> = (props: Props) => {
|
|||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [description, setDescription] = useState<string>('')
|
const [description, setDescription] = useState<string>('')
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<DefaulModal
|
<DefaulModal
|
||||||
open={props.isOpen}
|
open={props.isOpen}
|
||||||
close={props.close}
|
close={props.close}
|
||||||
title_header={t('confrim.subject')}
|
title_header={t('confrim.subject')}
|
||||||
isHeader
|
isHeader
|
||||||
|
elevated
|
||||||
>
|
>
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<div className='text-sm text-center'>
|
<div className='text-sm text-center'>
|
||||||
@@ -60,7 +62,8 @@ const ModalConfrim: FC<Props> = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DefaulModal>
|
</DefaulModal>,
|
||||||
|
document.body,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ 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 { UserItemType } from "../../../users/types/UserTypes";
|
||||||
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
||||||
import { useCreateCheckListItem, useCreateTaskAttachment, useCreateTaskRemark, useUpdateTask } from "../../task/hooks/useTaskData";
|
import {
|
||||||
|
useCreateCheckListItem,
|
||||||
|
useCreateTaskAttachment,
|
||||||
|
useCreateTaskRemark,
|
||||||
|
useDeleteTaskRemark,
|
||||||
|
useUpdateTask,
|
||||||
|
} from "../../task/hooks/useTaskData";
|
||||||
import { useSingleUpload } from "../../../service/hooks/useServiceData";
|
import { useSingleUpload } from "../../../service/hooks/useServiceData";
|
||||||
import { ErrorType } from "../../../../helpers/types";
|
import { ErrorType } from "../../../../helpers/types";
|
||||||
|
|
||||||
@@ -31,6 +37,7 @@ type Props = {
|
|||||||
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
|
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const createTaskRemark = useCreateTaskRemark();
|
const createTaskRemark = useCreateTaskRemark();
|
||||||
|
const deleteTaskRemark = useDeleteTaskRemark();
|
||||||
const createCheckListItem = useCreateCheckListItem();
|
const createCheckListItem = useCreateCheckListItem();
|
||||||
const createTaskAttachment = useCreateTaskAttachment();
|
const createTaskAttachment = useCreateTaskAttachment();
|
||||||
const singleUpload = useSingleUpload();
|
const singleUpload = useSingleUpload();
|
||||||
@@ -224,6 +231,29 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteLabel = (id: string) => {
|
||||||
|
const effectiveTaskId = taskId || taskDetail?.id;
|
||||||
|
if (!effectiveTaskId) return;
|
||||||
|
|
||||||
|
deleteTaskRemark.mutate(
|
||||||
|
{ id, taskId: effectiveTaskId },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setLabels((prev) => prev.filter((label) => label.id !== id));
|
||||||
|
setSelectedLabelIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
toast(t("success"), "success");
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleChecklistClose = () => {
|
const handleChecklistClose = () => {
|
||||||
if (isChecklistOpen) onTabChange("checklist");
|
if (isChecklistOpen) onTabChange("checklist");
|
||||||
};
|
};
|
||||||
@@ -338,7 +368,9 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
onViewChange={setLabelsView}
|
onViewChange={setLabelsView}
|
||||||
onToggle={handleLabelToggle}
|
onToggle={handleLabelToggle}
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
|
onDelete={handleDeleteLabel}
|
||||||
isSubmitting={createTaskRemark.isPending}
|
isSubmitting={createTaskRemark.isPending}
|
||||||
|
isDeleting={deleteTaskRemark.isPending}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AddCircle, Edit } from "iconsax-react";
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Button from "../../../../../components/Button";
|
import Button from "../../../../../components/Button";
|
||||||
|
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
||||||
import TaskDetailLabelCheckbox from "./TaskDetailLabelCheckbox";
|
import TaskDetailLabelCheckbox from "./TaskDetailLabelCheckbox";
|
||||||
import type { TaskLabel } from "./types";
|
import type { TaskLabel } from "./types";
|
||||||
|
|
||||||
@@ -10,10 +11,20 @@ type Props = {
|
|||||||
selectedIds: Set<string>;
|
selectedIds: Set<string>;
|
||||||
onToggle: (id: string) => void;
|
onToggle: (id: string) => void;
|
||||||
onEdit: (label: TaskLabel) => void;
|
onEdit: (label: TaskLabel) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
onCreateClick: () => void;
|
onCreateClick: () => void;
|
||||||
|
isDeleting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailLabelsList: FC<Props> = ({ labels, selectedIds, onToggle, onEdit, onCreateClick }) => {
|
const TaskDetailLabelsList: FC<Props> = ({
|
||||||
|
labels,
|
||||||
|
selectedIds,
|
||||||
|
onToggle,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onCreateClick,
|
||||||
|
isDeleting = false,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +43,8 @@ const TaskDetailLabelsList: FC<Props> = ({ labels, selectedIds, onToggle, onEdit
|
|||||||
{label.title && <span className="text-xs font-medium truncate">{label.title}</span>}
|
{label.title && <span className="text-xs font-medium truncate">{label.title}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Edit onClick={() => onEdit(label)} size={16} color="#0047FF" />
|
<Edit onClick={() => onEdit(label)} size={16} color="#0047FF" className="shrink-0 cursor-pointer" />
|
||||||
|
<TrashWithConfrim onDelete={() => onDelete(label.id)} isLoading={isDeleting} size={16} color="#FF0000" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ type Props = {
|
|||||||
onViewChange: (view: LabelsView) => void;
|
onViewChange: (view: LabelsView) => void;
|
||||||
onToggle: (id: string) => void;
|
onToggle: (id: string) => void;
|
||||||
onCreate: (title: string, color: string) => void;
|
onCreate: (title: string, color: string) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
|
isDeleting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailLabelsPopover: FC<Props> = ({
|
const TaskDetailLabelsPopover: FC<Props> = ({
|
||||||
@@ -20,7 +22,9 @@ const TaskDetailLabelsPopover: FC<Props> = ({
|
|||||||
onViewChange,
|
onViewChange,
|
||||||
onToggle,
|
onToggle,
|
||||||
onCreate,
|
onCreate,
|
||||||
|
onDelete,
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
|
isDeleting = false,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -30,7 +34,9 @@ const TaskDetailLabelsPopover: FC<Props> = ({
|
|||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
onEdit={() => undefined}
|
onEdit={() => undefined}
|
||||||
|
onDelete={onDelete}
|
||||||
onCreateClick={() => onViewChange("create")}
|
onCreateClick={() => onViewChange("create")}
|
||||||
|
isDeleting={isDeleting}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TaskDetailNewLabelForm
|
<TaskDetailNewLabelForm
|
||||||
|
|||||||
@@ -119,6 +119,18 @@ export const useCreateTaskRemark = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDeleteTaskRemark = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id }: { id: string; taskId: string }) => api.deleteTaskRemark(id),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreateCheckListItem = () => {
|
export const useCreateCheckListItem = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ export const createTaskRemark = async (params: CreateTaskRemarkType): Promise<Cr
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteTaskRemark = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/task-manager/remarks/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const createCheckListItem = async (params: CreateCheckListItemType): Promise<CreateCheckListItemResponse> => {
|
export const createCheckListItem = async (params: CreateCheckListItemType): Promise<CreateCheckListItemResponse> => {
|
||||||
const { data } = await axios.post(`/task-manager/check-list-items`, params);
|
const { data } = await axios.post(`/task-manager/check-list-items`, params);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
Reference in New Issue
Block a user