This commit is contained in:
@@ -72,6 +72,82 @@ tbody tr {
|
||||
.rmdp-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-wrapper {
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-calendar {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-header {
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-header-values {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #292d32;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-arrow {
|
||||
border-color: #292d32;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-week-day {
|
||||
font-size: 11px;
|
||||
color: #8c90a3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day span {
|
||||
font-size: 11px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-deactive span {
|
||||
color: #c4c4c4;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-today span {
|
||||
background-color: transparent;
|
||||
color: #292d32;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day:not(.rmdp-disabled):not(.rmdp-day-hidden) span:hover {
|
||||
background-color: #f0edf5;
|
||||
color: #292d32;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-selected span:not(.highlight) {
|
||||
background-color: #292d32;
|
||||
color: #fff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-range {
|
||||
background-color: #f0edf5;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-range.start,
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-range.end {
|
||||
background-color: transparent;
|
||||
}
|
||||
.dltr {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
+3
-1
@@ -1006,7 +1006,9 @@
|
||||
"none": "هیچ کدام",
|
||||
"link": "لینک",
|
||||
"or": "یا",
|
||||
"insert": "درج"
|
||||
"insert": "درج",
|
||||
"start_date": "تاریخ شروع",
|
||||
"end_date": "تاریخ پایان"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Calendar, Paperclip2, Profile2User, Tag, TickSquare, type Icon } from "iconsax-react";
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import { type FC, type ReactNode, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TaskDetailPopoverPanel from "./TaskDetailPopoverPanel";
|
||||
import type { TaskDetailTab } from "./types";
|
||||
|
||||
type TabItem = {
|
||||
@@ -24,49 +25,67 @@ type Props = {
|
||||
usersPopover?: ReactNode;
|
||||
checklistPopover?: ReactNode;
|
||||
attachmentPopover?: ReactNode;
|
||||
datePopover?: ReactNode;
|
||||
};
|
||||
|
||||
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover, usersPopover, checklistPopover, attachmentPopover }) => {
|
||||
const POPOVER_TABS = new Set<TaskDetailTab>(["labels", "users", "checklist", "attachment", "date"]);
|
||||
|
||||
type PopoverTab = "labels" | "users" | "checklist" | "attachment" | "date";
|
||||
|
||||
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover, usersPopover, checklistPopover, attachmentPopover, datePopover }) => {
|
||||
const { t } = useTranslation("global");
|
||||
const anchorRefs = useRef<Record<PopoverTab, { current: HTMLElement | null }>>({
|
||||
labels: { current: null },
|
||||
users: { current: null },
|
||||
checklist: { current: null },
|
||||
attachment: { current: null },
|
||||
date: { current: null },
|
||||
});
|
||||
|
||||
const popoverByTab: Record<PopoverTab, ReactNode> = {
|
||||
labels: labelsPopover,
|
||||
users: usersPopover,
|
||||
checklist: checklistPopover,
|
||||
attachment: attachmentPopover,
|
||||
date: datePopover,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{TABS.map(({ id, icon: Icon, labelKey }) => {
|
||||
const isActive = activeTab === id;
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{TABS.map(({ id, icon: Icon, labelKey }) => {
|
||||
const isActive = activeTab === id;
|
||||
const hasPopover = POPOVER_TABS.has(id);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onTabChange(id)}
|
||||
className={`flex items-center gap-1.5 border rounded-lg px-3 py-2 text-[11px] cursor-pointer transition-colors ${isActive ? "bg-[#4A4A4A] text-white border-transparent" : "bg-white/50 text-[#292D32] border-white"}`}
|
||||
>
|
||||
<Icon size={16} color={isActive ? "#FFFFFF" : "#292D32"} />
|
||||
<span>{t(labelKey)}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (id === "labels" || id === "users" || id === "checklist" || id === "attachment") {
|
||||
return (
|
||||
<div key={id} className="relative">
|
||||
{button}
|
||||
{id === "labels"
|
||||
? labelsPopover
|
||||
: id === "users"
|
||||
? usersPopover
|
||||
: id === "checklist"
|
||||
? checklistPopover
|
||||
: attachmentPopover}
|
||||
<div
|
||||
key={id}
|
||||
ref={(element) => {
|
||||
if (!hasPopover) return;
|
||||
anchorRefs.current[id as PopoverTab].current = element;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onTabChange(id)}
|
||||
className={`flex items-center gap-1.5 border rounded-lg px-3 py-2 text-[11px] cursor-pointer transition-colors ${isActive ? "bg-[#4A4A4A] text-white border-transparent" : "bg-white/50 text-[#292D32] border-white"}`}
|
||||
>
|
||||
<Icon size={16} color={isActive ? "#FFFFFF" : "#292D32"} />
|
||||
<span>{t(labelKey)}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div key={id}>
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{(Object.keys(popoverByTab) as PopoverTab[]).map((tab) =>
|
||||
popoverByTab[tab] ? (
|
||||
<TaskDetailPopoverPanel key={tab} anchorRef={anchorRefs.current[tab]}>
|
||||
{popoverByTab[tab]}
|
||||
</TaskDetailPopoverPanel>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { type FC, type ReactNode, useLayoutEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { clx } from "../../../../helpers/utils";
|
||||
|
||||
type Props = {
|
||||
anchorRef: { current: HTMLElement | null };
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, width = 300 }) => {
|
||||
const [position, setPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updatePosition = () => {
|
||||
const anchor = anchorRef.current;
|
||||
if (!anchor) return;
|
||||
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const padding = 8;
|
||||
let left = rect.right - width;
|
||||
left = Math.max(padding, Math.min(left, window.innerWidth - width - padding));
|
||||
|
||||
setPosition({
|
||||
top: rect.bottom + 8,
|
||||
left,
|
||||
});
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
window.addEventListener("resize", updatePosition);
|
||||
window.addEventListener("scroll", updatePosition, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updatePosition);
|
||||
window.removeEventListener("scroll", updatePosition, true);
|
||||
};
|
||||
}, [anchorRef, width]);
|
||||
|
||||
if (!position) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
style={{ top: position.top, left: position.left, width }}
|
||||
className={clx("fixed z-[80] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80", className)}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDetailPopoverPanel;
|
||||
@@ -1,5 +1,7 @@
|
||||
import { type FC, useState } from "react";
|
||||
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
|
||||
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
|
||||
import type DateObject from "react-date-object";
|
||||
import { DEFAULT_CHECKLISTS } from "./checklist/constants";
|
||||
import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover";
|
||||
import type { TaskChecklist } from "./checklist/types";
|
||||
@@ -30,6 +32,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
|
||||
const isUsersOpen = activeTab === "users";
|
||||
const isChecklistOpen = activeTab === "checklist";
|
||||
const isAttachmentOpen = activeTab === "attachment";
|
||||
const isDateOpen = activeTab === "date";
|
||||
|
||||
const handleTabChange = (tab: TaskDetailTab) => {
|
||||
if (tab === "labels" && activeTab !== "labels") {
|
||||
@@ -94,6 +97,14 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
|
||||
handleAttachmentClose();
|
||||
};
|
||||
|
||||
const handleDateClose = () => {
|
||||
if (isDateOpen) onTabChange("date");
|
||||
};
|
||||
|
||||
const handleDateSubmit = (_data: { startDate: DateObject | null; endDate: DateObject | null }) => {
|
||||
handleDateClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<TaskDetailActionBar
|
||||
@@ -130,6 +141,11 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
|
||||
<TaskDetailAttachmentPopover onClose={handleAttachmentClose} onSubmit={handleAttachmentCreate} />
|
||||
) : null
|
||||
}
|
||||
datePopover={
|
||||
isDateOpen ? (
|
||||
<TaskDetailDatePopover onClose={handleDateClose} onSubmit={handleDateSubmit} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
<TaskDetailMetadataPreview selectedLabels={selectedLabels} selectedUsers={selectedUsers} />
|
||||
|
||||
+1
-5
@@ -7,11 +7,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const TaskDetailAttachmentPopover: FC<Props> = ({ onClose, onSubmit }) => {
|
||||
return (
|
||||
<div className="absolute top-full right-0 mt-2 z-30 w-[300px] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80">
|
||||
<TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} />
|
||||
</div>
|
||||
);
|
||||
return <TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} />;
|
||||
};
|
||||
|
||||
export default TaskDetailAttachmentPopover;
|
||||
|
||||
+1
-5
@@ -9,11 +9,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const TaskDetailChecklistPopover: FC<Props> = ({ checklists, onClose, onSubmit }) => {
|
||||
return (
|
||||
<div className="absolute top-full right-0 mt-2 z-30 w-[300px] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80">
|
||||
<TaskDetailChecklistForm checklists={checklists} onClose={onClose} onSubmit={onSubmit} />
|
||||
</div>
|
||||
);
|
||||
return <TaskDetailChecklistForm checklists={checklists} onClose={onClose} onSubmit={onSubmit} />;
|
||||
};
|
||||
|
||||
export default TaskDetailChecklistPopover;
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { type FC, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DateObject from "react-date-object";
|
||||
import persian from "react-date-object/calendars/persian";
|
||||
import persian_fa from "react-date-object/locales/persian_fa";
|
||||
import { Calendar } from "react-multi-date-picker";
|
||||
import Button from "../../../../../components/Button";
|
||||
import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox";
|
||||
import type { TaskDateField } from "./types";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSubmit: (data: { startDate: DateObject | null; endDate: DateObject | null }) => void;
|
||||
};
|
||||
|
||||
const formatDateLabel = (date: DateObject | null) => {
|
||||
if (!date) return "";
|
||||
return date.convert(persian, persian_fa).format("D MMMM");
|
||||
};
|
||||
|
||||
const TaskDetailDateForm: FC<Props> = ({ onClose, onSubmit }) => {
|
||||
const { t } = useTranslation("global");
|
||||
const [startDate, setStartDate] = useState<DateObject | null>(null);
|
||||
const [endDate, setEndDate] = useState<DateObject | null>(null);
|
||||
const [startEnabled, setStartEnabled] = useState(true);
|
||||
const [endEnabled, setEndEnabled] = useState(false);
|
||||
const [activeField, setActiveField] = useState<TaskDateField>("start");
|
||||
|
||||
const calendarValue = (() => {
|
||||
if (startEnabled && endEnabled && startDate && endDate) {
|
||||
return [startDate, endDate];
|
||||
}
|
||||
if (activeField === "end" && endDate) return endDate;
|
||||
if (startDate) return startDate;
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
const handleCalendarChange = (value: DateObject | DateObject[] | null) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const [start, end] = value;
|
||||
if (startEnabled) setStartDate(start);
|
||||
if (endEnabled) setEndDate(end ?? null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeField === "start" && startEnabled) {
|
||||
setStartDate(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeField === "end" && endEnabled) {
|
||||
setEndDate(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartToggle = () => {
|
||||
const next = !startEnabled;
|
||||
setStartEnabled(next);
|
||||
if (next) {
|
||||
setActiveField("start");
|
||||
return;
|
||||
}
|
||||
if (!endEnabled) {
|
||||
setEndEnabled(true);
|
||||
setActiveField("end");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndToggle = () => {
|
||||
const next = !endEnabled;
|
||||
setEndEnabled(next);
|
||||
if (next) {
|
||||
setActiveField("end");
|
||||
return;
|
||||
}
|
||||
if (!startEnabled) {
|
||||
setStartEnabled(true);
|
||||
setActiveField("start");
|
||||
}
|
||||
};
|
||||
|
||||
const canSubmit = (startEnabled && Boolean(startDate)) || (endEnabled && Boolean(endDate));
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!canSubmit) return;
|
||||
onSubmit({
|
||||
startDate: startEnabled ? startDate : null,
|
||||
endDate: endEnabled ? endDate : null,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="task-detail-date-calendar">
|
||||
<Calendar
|
||||
value={calendarValue}
|
||||
onChange={handleCalendarChange}
|
||||
calendar={persian}
|
||||
locale={persian_fa}
|
||||
range={startEnabled && endEnabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs shrink-0 w-[72px]">{t("taskmanager.task_detail.start_date")}</span>
|
||||
<input
|
||||
readOnly
|
||||
value={formatDateLabel(startDate)}
|
||||
placeholder={t("taskmanager.select_date")}
|
||||
onFocus={() => setActiveField("start")}
|
||||
className="flex-1 h-8 text-xs rounded-xl border border-white bg-white/63 px-3 text-[#292D32] outline-none"
|
||||
/>
|
||||
<TaskDetailLabelCheckbox
|
||||
checked={startEnabled}
|
||||
onToggle={handleStartToggle}
|
||||
ariaLabel={t("taskmanager.task_detail.start_date")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs shrink-0 w-[72px]">{t("taskmanager.task_detail.end_date")}</span>
|
||||
<input
|
||||
readOnly
|
||||
value={formatDateLabel(endDate)}
|
||||
placeholder={t("taskmanager.select_date")}
|
||||
onFocus={() => setActiveField("end")}
|
||||
className="flex-1 h-8 text-xs rounded-xl border border-white bg-white/63 px-3 text-[#292D32] outline-none"
|
||||
/>
|
||||
<TaskDetailLabelCheckbox
|
||||
checked={endEnabled}
|
||||
onToggle={handleEndToggle}
|
||||
ariaLabel={t("taskmanager.task_detail.end_date")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" onClick={onClose} className="h-8 bg-[#E8E4F0] text-[#292D32] rounded-xl text-xs w-[95px]">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSubmit} disabled={!canSubmit} className="h-8 bg-[#292D32] text-white rounded-xl text-xs disabled:opacity-50 w-[95px]">
|
||||
{t("taskmanager.task_detail.insert")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDetailDateForm;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type FC } from "react";
|
||||
import type DateObject from "react-date-object";
|
||||
import TaskDetailDateForm from "./TaskDetailDateForm";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSubmit: (data: { startDate: DateObject | null; endDate: DateObject | null }) => void;
|
||||
};
|
||||
|
||||
const TaskDetailDatePopover: FC<Props> = ({ onClose, onSubmit }) => {
|
||||
return <TaskDetailDateForm onClose={onClose} onSubmit={onSubmit} />;
|
||||
};
|
||||
|
||||
export default TaskDetailDatePopover;
|
||||
@@ -0,0 +1,10 @@
|
||||
import type DateObject from "react-date-object";
|
||||
|
||||
export type TaskDateField = "start" | "end";
|
||||
|
||||
export type TaskDateRange = {
|
||||
startDate: DateObject | null;
|
||||
endDate: DateObject | null;
|
||||
startEnabled: boolean;
|
||||
endEnabled: boolean;
|
||||
};
|
||||
@@ -21,7 +21,7 @@ const TaskDetailLabelsPopover: FC<Props> = ({
|
||||
onCreate,
|
||||
}) => {
|
||||
return (
|
||||
<div className="absolute top-full right-0 mt-2 z-30 w-[300px] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80">
|
||||
<>
|
||||
{view === "list" ? (
|
||||
<TaskDetailLabelsList
|
||||
labels={labels}
|
||||
@@ -36,7 +36,7 @@ const TaskDetailLabelsPopover: FC<Props> = ({
|
||||
onSubmit={onCreate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,11 +9,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle }) => {
|
||||
return (
|
||||
<div className="absolute top-full right-0 mt-2 z-30 w-[300px] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80">
|
||||
<TaskDetailUsersList users={users} selectedIds={selectedIds} onToggle={onToggle} />
|
||||
</div>
|
||||
);
|
||||
return <TaskDetailUsersList users={users} selectedIds={selectedIds} onToggle={onToggle} />;
|
||||
};
|
||||
|
||||
export default TaskDetailUsersPopover;
|
||||
|
||||
Reference in New Issue
Block a user