551 lines
24 KiB
TypeScript
551 lines
24 KiB
TypeScript
/**
|
||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||
* SPDX-License-Identifier: AGPL-3.0-only
|
||
* See the LICENSE file for details.
|
||
*/
|
||
|
||
import { useMemo, useState } from "react";
|
||
import { useSearchParams } from "next/navigation";
|
||
import { Archive, CalendarDays, Copy, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
|
||
import { observer } from "mobx-react";
|
||
import { ARCHIVABLE_STATE_GROUPS, EUserPermissions } from "@plane/constants";
|
||
import { useTranslation } from "@plane/i18n";
|
||
import { PriorityIcon, StateGroupIcon, getStateGroupColor } from "@plane/propel/icons";
|
||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||
import type { IState, TExternalContourBoardDirection, TExternalContourRequest, TIssue } from "@plane/types";
|
||
import { ActionDropdown, Avatar } from "@plane/ui";
|
||
import { cn, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
||
import { DateDropdown } from "@/components/dropdowns/date";
|
||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
|
||
import { MemberDropdownBase } from "@/components/dropdowns/member/base";
|
||
import { PresenceDot } from "@/components/presence/presence-dot";
|
||
import { useAppRouter } from "@/hooks/use-app-router";
|
||
import { useMember } from "@/hooks/store/use-member";
|
||
import { useProjectExternalContoursBoard } from "@/hooks/store/use-project-external-contours-board";
|
||
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||
import { useUserPermissions } from "@/hooks/store/user";
|
||
import { IssueArchiveService } from "@/services/issue/issue_archive.service";
|
||
import { IssueService } from "@/services/issue/issue.service";
|
||
import { NodedcWorkItemProgress } from "@/components/issues/issue-layouts/shared/nodedc-work-item-card";
|
||
import { ExternalContourDeleteModal } from "./delete-modal";
|
||
|
||
type Props = {
|
||
direction: TExternalContourBoardDirection;
|
||
projectId: string;
|
||
request: TExternalContourRequest;
|
||
workspaceSlug: string;
|
||
};
|
||
|
||
const issueService = new IssueService();
|
||
const issueArchiveService = new IssueArchiveService();
|
||
|
||
const basePillClasses =
|
||
"inline-flex min-h-8 items-center gap-1.5 rounded-full border-0 px-2.5 py-1 text-[10px] font-medium shadow-none outline-none transition-colors";
|
||
|
||
const buildSourceStateMap = (
|
||
states: { id: string; name: string; color: string; group: IState["group"] }[] | undefined,
|
||
projectId: string | null
|
||
): Record<string, IState> =>
|
||
Object.fromEntries(
|
||
(states ?? []).map((state, index) => [
|
||
state.id,
|
||
{
|
||
id: state.id,
|
||
color: getStateGroupColor(state.group, state.color),
|
||
default: false,
|
||
description: "",
|
||
group: state.group,
|
||
name: state.name,
|
||
order: index + 1,
|
||
project_id: projectId ?? "",
|
||
sequence: index + 1,
|
||
workspace_id: "",
|
||
} satisfies IState,
|
||
])
|
||
);
|
||
|
||
const resolveRequestStatus = (
|
||
issue: TExternalContourRequest["issue"],
|
||
fallbackStatus: TExternalContourRequest["status"]
|
||
) => {
|
||
const stateGroup = issue.state_detail?.group;
|
||
if (!stateGroup) return fallbackStatus;
|
||
return stateGroup === "completed" || stateGroup === "cancelled" ? "closed" : "open";
|
||
};
|
||
|
||
export const ExternalContoursBoardItem = observer(function ExternalContoursBoardItem(props: Props) {
|
||
const { direction, projectId, request, workspaceSlug } = props;
|
||
const router = useAppRouter();
|
||
const searchParams = useSearchParams();
|
||
const { t } = useTranslation();
|
||
const { getUserDetails, workspace } = useMember();
|
||
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||
const { getStateById, getProjectStateIds } = useProjectState();
|
||
const { fetchBoard, removeBoardItem, upsertBoardItems } = useProjectExternalContoursBoard();
|
||
const { deleteRequest, fetchTargetOptions, getTargetOptionsByProjectId, updateRequest, updateRequestIssue } =
|
||
useProjectExternalContours();
|
||
const [isUpdating, setIsUpdating] = useState(false);
|
||
const [isSourceOptionsLoading, setIsSourceOptionsLoading] = useState(false);
|
||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||
|
||
const issue = request.issue;
|
||
const selectedInboxIssueId = searchParams.get("inboxIssueId");
|
||
const isActive = selectedInboxIssueId === request.id;
|
||
const requester =
|
||
request.requested_by?.display_name ||
|
||
request.requested_by_name ||
|
||
issue.created_by_detail?.display_name ||
|
||
"NODE.DC";
|
||
const requesterId = request.requested_by?.id || request.requested_by_id || issue.created_by_detail?.id || issue.created_by;
|
||
const requesterAvatar = issue.created_by_detail?.avatar_url || "";
|
||
const counterpartContourName =
|
||
direction === "outgoing"
|
||
? request.target_project?.name || request.target_project_name || issue.project_detail?.name
|
||
: request.source_project?.name || request.source_project_name;
|
||
const targetProjectId = issue.project_id || request.target_project?.id || request.target_project_id || null;
|
||
const projectRole = targetProjectId
|
||
? getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId)
|
||
: undefined;
|
||
const canEditTargetIssue =
|
||
direction === "incoming" &&
|
||
!!targetProjectId &&
|
||
projectRole !== undefined &&
|
||
projectRole !== EUserPermissions.GUEST;
|
||
const canEditSourceRequest =
|
||
direction === "outgoing" && !!request.capabilities?.can_edit_request && !!targetProjectId;
|
||
const canEditCard = canEditTargetIssue || canEditSourceRequest;
|
||
const requestLink = `/${workspaceSlug}/projects/${projectId}/external-contours?inboxIssueId=${request.id}`;
|
||
const targetOptions = getTargetOptionsByProjectId(targetProjectId);
|
||
const sourceStateMap = useMemo(
|
||
() => buildSourceStateMap(targetOptions?.states, targetProjectId),
|
||
[targetOptions?.states, targetProjectId]
|
||
);
|
||
const sourceStateIds = useMemo(() => targetOptions?.states?.map((state) => state.id) ?? [], [targetOptions?.states]);
|
||
const selectedState = canEditTargetIssue ? getStateById(issue.state_id) : sourceStateMap[issue.state_id ?? ""];
|
||
const projectStateIds = issue.project_id ? (getProjectStateIds(issue.project_id) ?? []) : [];
|
||
const stateOptions = canEditTargetIssue
|
||
? projectStateIds.map((stateId) => getStateById(stateId)).filter((state): state is IState => !!state)
|
||
: sourceStateIds.map((stateId) => sourceStateMap[stateId]).filter((state): state is IState => !!state);
|
||
const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white";
|
||
const subtleTextClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/70" : "text-[#B3B3B8]";
|
||
const pillBackgroundClasses = isActive
|
||
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
||
: "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";
|
||
const cornerActionButtonClasses = cn(
|
||
"flex h-12 w-12 -translate-x-0.5 -translate-y-0.5 items-center justify-center rounded-full border bg-transparent shadow-none ring-0 transition-colors outline-none",
|
||
isActive
|
||
? "border-black/25 text-black hover:bg-black/5"
|
||
: "border-white/20 text-white hover:border-white/35 hover:bg-white/5"
|
||
);
|
||
const assigneeButtonClasses = cn(
|
||
"flex h-7 min-w-7 items-center justify-center rounded-full border-0 bg-transparent p-0 shadow-none outline-none transition-colors",
|
||
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white"
|
||
);
|
||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||
const checkerBlocksTotal = issue.checker_blocks_count ?? 0;
|
||
const checkerItemsTotal = issue.checker_items_count ?? 0;
|
||
const checkerItemsCompleted = issue.checker_items_completed_count ?? 0;
|
||
const hasCheckerProgress = checkerBlocksTotal > 0;
|
||
const canArchive = canEditTargetIssue && !!selectedState && ARCHIVABLE_STATE_GROUPS.includes(selectedState.group);
|
||
|
||
if (!issue) return null;
|
||
|
||
const stopCardPropagation = (event: React.MouseEvent) => {
|
||
event.stopPropagation();
|
||
};
|
||
|
||
const openDetail = () => {
|
||
if (isActive) return;
|
||
router.push(requestLink);
|
||
};
|
||
|
||
const syncBoardAfterMutation = async () => {
|
||
await fetchBoard(workspaceSlug, projectId);
|
||
};
|
||
|
||
const ensureSourceOptions = async () => {
|
||
if (!canEditSourceRequest || !targetProjectId) return;
|
||
|
||
const tasks: Promise<unknown>[] = [];
|
||
if (!targetOptions) {
|
||
setIsSourceOptionsLoading(true);
|
||
tasks.push(fetchTargetOptions(workspaceSlug, projectId, targetProjectId));
|
||
}
|
||
if (!workspace.workspaceMemberIds) {
|
||
tasks.push(workspace.fetchWorkspaceMembers(workspaceSlug));
|
||
}
|
||
|
||
if (tasks.length === 0) return;
|
||
|
||
try {
|
||
await Promise.all(tasks);
|
||
} finally {
|
||
setIsSourceOptionsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleTargetIssueUpdate = async (data: Partial<TIssue>) => {
|
||
if (!targetProjectId || !issue.id || isUpdating) return;
|
||
|
||
setIsUpdating(true);
|
||
try {
|
||
const updatedIssue = await issueService.patchIssue(workspaceSlug, targetProjectId, issue.id, data);
|
||
const nextIssue = { ...issue, ...updatedIssue };
|
||
const nextRequest = {
|
||
...request,
|
||
issue: nextIssue,
|
||
status: resolveRequestStatus(nextIssue, request.status),
|
||
};
|
||
|
||
updateRequestIssue(request.id, nextIssue);
|
||
upsertBoardItems([nextRequest]);
|
||
await syncBoardAfterMutation();
|
||
} catch {
|
||
setToast({ title: t("error"), type: TOAST_TYPE.ERROR, message: t("issue_could_not_be_updated") });
|
||
} finally {
|
||
setIsUpdating(false);
|
||
}
|
||
};
|
||
|
||
const handleSourceRequestUpdate = async (data: Partial<TIssue>) => {
|
||
if (!canEditSourceRequest || isUpdating) return;
|
||
|
||
setIsUpdating(true);
|
||
try {
|
||
const updatedRequest = await updateRequest(workspaceSlug, projectId, request.id, data);
|
||
if (updatedRequest) {
|
||
upsertBoardItems([updatedRequest]);
|
||
}
|
||
await syncBoardAfterMutation();
|
||
} catch {
|
||
setToast({ title: t("error"), type: TOAST_TYPE.ERROR, message: t("issue_could_not_be_updated") });
|
||
} finally {
|
||
setIsUpdating(false);
|
||
}
|
||
};
|
||
|
||
const handleCardUpdate = async (data: Partial<TIssue>) => {
|
||
if (canEditTargetIssue) {
|
||
await handleTargetIssueUpdate(data);
|
||
return;
|
||
}
|
||
|
||
await handleSourceRequestUpdate(data);
|
||
};
|
||
|
||
const handleCopyLink = async () => {
|
||
const absoluteLink = `${window.location.origin}${requestLink}`;
|
||
await navigator.clipboard?.writeText(absoluteLink);
|
||
setToast({ title: "Ссылка скопирована", type: TOAST_TYPE.SUCCESS });
|
||
};
|
||
|
||
const handleArchiveIssue = async () => {
|
||
if (!targetProjectId || !issue.id || !canArchive || isUpdating) return;
|
||
|
||
setIsUpdating(true);
|
||
try {
|
||
await issueArchiveService.archiveIssue(workspaceSlug, targetProjectId, issue.id);
|
||
await syncBoardAfterMutation();
|
||
setToast({ title: "Задача архивирована", type: TOAST_TYPE.SUCCESS });
|
||
} catch {
|
||
setToast({ title: t("error"), type: TOAST_TYPE.ERROR, message: "Не удалось архивировать задачу" });
|
||
} finally {
|
||
setIsUpdating(false);
|
||
}
|
||
};
|
||
|
||
const handleDeleteRequest = async () => {
|
||
if (direction !== "outgoing" || isUpdating) return;
|
||
|
||
setIsUpdating(true);
|
||
try {
|
||
await deleteRequest(workspaceSlug, projectId, request.id);
|
||
removeBoardItem(request.id);
|
||
if (isActive) router.push(`/${workspaceSlug}/projects/${projectId}/external-contours`);
|
||
setToast({ title: "Исходящая задача удалена", type: TOAST_TYPE.SUCCESS });
|
||
setIsDeleteModalOpen(false);
|
||
} catch {
|
||
setToast({ title: t("error"), type: TOAST_TYPE.ERROR, message: "Не удалось удалить исходящую задачу" });
|
||
} finally {
|
||
setIsUpdating(false);
|
||
}
|
||
};
|
||
|
||
const priorityOptions: NonNullable<TIssue["priority"]>[] = ["urgent", "high", "medium", "low", "none"];
|
||
const priorityLabels: Record<NonNullable<TIssue["priority"]>, string> = {
|
||
urgent: "Срочный",
|
||
high: "Высокий",
|
||
medium: "Средний",
|
||
low: "Низкий",
|
||
none: "Без приоритета",
|
||
};
|
||
|
||
const menuItemClasses =
|
||
"flex w-full items-center gap-2 rounded-[0.9rem] px-2.5 py-2 text-left text-12 text-secondary transition-colors hover:bg-white/6 disabled:cursor-not-allowed disabled:text-placeholder disabled:hover:bg-transparent";
|
||
|
||
return (
|
||
<>
|
||
<ExternalContourDeleteModal
|
||
isOpen={isDeleteModalOpen}
|
||
isSubmitting={isUpdating}
|
||
issueName={issue.name}
|
||
onClose={() => setIsDeleteModalOpen(false)}
|
||
onSubmit={handleDeleteRequest}
|
||
/>
|
||
<div className="group/kanban-block relative mb-2">
|
||
<div
|
||
data-active={isActive}
|
||
data-priority={issue.priority ?? "none"}
|
||
className="nodedc-external-card relative flex min-h-[220px] w-full cursor-pointer flex-col p-4 transition-all hover:bg-white/5"
|
||
role="button"
|
||
tabIndex={0}
|
||
onClick={openDetail}
|
||
onKeyDown={(event) => {
|
||
if (event.key === "Enter" || event.key === " ") {
|
||
event.preventDefault();
|
||
openDetail();
|
||
}
|
||
}}
|
||
>
|
||
<div className={cn("relative flex min-h-[220px] flex-col px-1", foregroundClasses)}>
|
||
<div className="absolute top-0.5 left-0.5 z-20">
|
||
<div className="relative h-12 w-12">
|
||
<Avatar
|
||
src={requesterAvatar}
|
||
name={requester}
|
||
size={48}
|
||
className="border border-white/10 shadow-none ring-0 outline-none"
|
||
/>
|
||
<PresenceDot
|
||
workspaceSlug={workspaceSlug}
|
||
userId={requesterId}
|
||
className={isActive ? "border-[rgb(var(--nodedc-card-active-rgb))]" : undefined}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="absolute top-0.5 right-0.5 z-20" onClick={stopCardPropagation}>
|
||
<ActionDropdown
|
||
placement="bottom-end"
|
||
button={
|
||
<div className={cornerActionButtonClasses}>
|
||
<MoreHorizontal className="h-4 w-4" />
|
||
</div>
|
||
}
|
||
buttonClassName="h-12 w-12"
|
||
menuClassName="nodedc-work-item-action-menu"
|
||
onOpenChange={(isOpen) => {
|
||
if (isOpen) void ensureSourceOptions();
|
||
}}
|
||
items={[]}
|
||
menuContent={({ closeDropdown }) => (
|
||
<div className="nodedc-work-item-action-grid" onClick={stopCardPropagation}>
|
||
<div className="nodedc-work-item-action-section space-y-1">
|
||
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
|
||
Приоритет
|
||
</div>
|
||
{priorityOptions.map((priority) => (
|
||
<button
|
||
key={priority}
|
||
type="button"
|
||
className={cn(menuItemClasses, { "bg-white/7 text-primary": issue.priority === priority })}
|
||
disabled={!canEditCard || isUpdating}
|
||
onClick={() => {
|
||
void handleCardUpdate({ priority });
|
||
closeDropdown();
|
||
}}
|
||
>
|
||
<PriorityIcon priority={priority} className="h-3.5 w-3.5" />
|
||
<span>{priorityLabels[priority]}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="nodedc-work-item-action-section space-y-1">
|
||
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
|
||
Статус
|
||
</div>
|
||
{isSourceOptionsLoading && stateOptions.length === 0 ? (
|
||
<div className="px-2.5 py-2 text-12 text-tertiary">Загрузка статусов...</div>
|
||
) : (
|
||
stateOptions.map((state) => (
|
||
<button
|
||
key={state.id}
|
||
type="button"
|
||
className={cn(menuItemClasses, { "bg-white/7 text-primary": issue.state_id === state.id })}
|
||
disabled={!canEditCard || isUpdating}
|
||
onClick={() => {
|
||
void handleCardUpdate({ state_id: state.id });
|
||
closeDropdown();
|
||
}}
|
||
>
|
||
<StateGroupIcon
|
||
stateGroup={state.group}
|
||
color={getStateGroupColor(state.group, state.color)}
|
||
className="h-3.5 w-3.5"
|
||
percentage={state.order}
|
||
/>
|
||
<span>{state.name}</span>
|
||
</button>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
<div className="nodedc-work-item-action-section space-y-1">
|
||
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
|
||
Быстрые действия
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className={menuItemClasses}
|
||
onClick={() => {
|
||
router.push(requestLink);
|
||
closeDropdown();
|
||
}}
|
||
>
|
||
<Pencil className="h-3.5 w-3.5" />
|
||
<span>Редактировать</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={menuItemClasses}
|
||
onClick={() => {
|
||
void handleCopyLink();
|
||
closeDropdown();
|
||
}}
|
||
>
|
||
<Copy className="h-3.5 w-3.5" />
|
||
<span>Копировать ссылку</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={menuItemClasses}
|
||
disabled={!canArchive || isUpdating}
|
||
onClick={() => {
|
||
void handleArchiveIssue();
|
||
closeDropdown();
|
||
}}
|
||
>
|
||
<Archive className="h-3.5 w-3.5" />
|
||
<span>Архивировать</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={cn(menuItemClasses, "text-red-300 hover:bg-red-500/10 disabled:text-placeholder")}
|
||
disabled={direction !== "outgoing" || isUpdating}
|
||
onClick={() => {
|
||
setIsDeleteModalOpen(true);
|
||
closeDropdown();
|
||
}}
|
||
>
|
||
<Trash2 className="h-3.5 w-3.5" />
|
||
<span>Удалить</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="min-w-0 pr-[58px] pl-[58px] pt-1">
|
||
<div className="flex min-w-0 items-center gap-1.5">
|
||
<div className={cn("truncate text-body-sm-medium leading-5", foregroundClasses)}>{requester}</div>
|
||
{request.has_unread_updates && (
|
||
<span
|
||
className={cn("size-2 shrink-0 rounded-full", isActive ? "bg-black/70" : "bg-accent-primary")}
|
||
title={t("external_contours_page.list.unread_updates")}
|
||
/>
|
||
)}
|
||
</div>
|
||
<div className={cn("truncate text-[10px] leading-3.5 font-medium", subtleTextClasses)}>
|
||
{counterpartContourName || t("common.none")}
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className={cn(
|
||
"flex flex-1 items-center pb-4",
|
||
hasCheckerProgress ? "justify-center px-0.5 pt-4 text-left" : "justify-start px-1 pt-7 text-left"
|
||
)}
|
||
>
|
||
{hasCheckerProgress ? (
|
||
<div className="flex w-full flex-col items-stretch gap-3">
|
||
<NodedcWorkItemProgress
|
||
completedCount={checkerItemsCompleted}
|
||
totalCount={checkerItemsTotal}
|
||
isActive={isActive}
|
||
/>
|
||
<div className="line-clamp-4 w-full text-left text-[15px] leading-5 font-medium">
|
||
{issue.name}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="line-clamp-5 max-w-full text-[15px] leading-5 font-medium">{issue.name}</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
||
{canEditTargetIssue ? (
|
||
<MemberDropdown
|
||
multiple
|
||
projectId={issue.project_id ?? undefined}
|
||
value={issue.assignee_ids ?? []}
|
||
onChange={(assigneeIds) => void handleCardUpdate({ assignee_ids: assigneeIds })}
|
||
disabled={!canEditCard || isUpdating}
|
||
buttonVariant="transparent-without-text"
|
||
button={
|
||
<div className={assigneeButtonClasses}>
|
||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size={26} />
|
||
</div>
|
||
}
|
||
/>
|
||
) : (
|
||
<MemberDropdownBase
|
||
multiple
|
||
getUserDetails={getUserDetails}
|
||
memberIds={targetOptions?.member_ids ?? []}
|
||
value={issue.assignee_ids ?? []}
|
||
onChange={(assigneeIds) => void handleCardUpdate({ assignee_ids: assigneeIds })}
|
||
disabled={!canEditCard || isUpdating || !targetProjectId}
|
||
onDropdownOpen={() => {
|
||
void ensureSourceOptions();
|
||
}}
|
||
buttonVariant="transparent-without-text"
|
||
button={
|
||
<div className={assigneeButtonClasses}>
|
||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size={26} />
|
||
</div>
|
||
}
|
||
/>
|
||
)}
|
||
|
||
<DateDropdown
|
||
value={issue.target_date}
|
||
rangePreview={{
|
||
from: issue.start_date,
|
||
to: issue.target_date,
|
||
}}
|
||
onChange={(targetDate) =>
|
||
void handleCardUpdate({
|
||
target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null,
|
||
})
|
||
}
|
||
disabled={!canEditCard || isUpdating}
|
||
buttonVariant="transparent-without-text"
|
||
button={
|
||
<div className={cn(basePillClasses, pillBackgroundClasses)}>
|
||
<CalendarDays className="h-3 w-3" />
|
||
<span className="truncate">{dueDateLabel}</span>
|
||
</div>
|
||
}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
});
|