UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: выравнивание карточек внутреннего контура
This commit is contained in:
parent
a8b6f9f9ce
commit
655ff7fc4a
|
|
@ -8,7 +8,7 @@ import { useMemo } from "react";
|
||||||
import { CalendarDays, MoreHorizontal } from "lucide-react";
|
import { CalendarDays, MoreHorizontal } from "lucide-react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { PriorityIcon, StateGroupIcon } from "@plane/propel/icons";
|
import { PriorityIcon, StateGroupIcon, getStateGroupColor } from "@plane/propel/icons";
|
||||||
import type { IIssueDisplayProperties, TIssue } from "@plane/types";
|
import type { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||||
import { Avatar } from "@plane/ui";
|
import { Avatar } from "@plane/ui";
|
||||||
import { cn, getFileURL, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
import { cn, getFileURL, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
||||||
|
|
@ -21,10 +21,7 @@ import { useMember } from "@/hooks/store/use-member";
|
||||||
import { useProject } from "@/hooks/store/use-project";
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
import {
|
import { NodedcWorkItemCard, getNodedcWorkItemCardAppearance } from "../shared/nodedc-work-item-card";
|
||||||
NodedcWorkItemCard,
|
|
||||||
getNodedcWorkItemCardAppearance,
|
|
||||||
} from "../shared/nodedc-work-item-card";
|
|
||||||
import type { TRenderQuickActions } from "../list/list-view-types";
|
import type { TRenderQuickActions } from "../list/list-view-types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -74,103 +71,17 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
const sourceContourName = issue.source_project_name ?? getProjectById(issue.project_id)?.name ?? t("common.none");
|
const sourceContourName = issue.source_project_name ?? getProjectById(issue.project_id)?.name ?? t("common.none");
|
||||||
const selectedState = getStateById(issue.state_id);
|
const selectedState = getStateById(issue.state_id);
|
||||||
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
||||||
const { pillBackgroundClasses, iconBubbleClasses } = getNodedcWorkItemCardAppearance(isActive);
|
const { pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
||||||
const statusIconColor =
|
const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color);
|
||||||
selectedState?.color ?? (isActive ? "rgb(var(--nodedc-on-card-active-rgb))" : "var(--text-color-primary)");
|
|
||||||
|
|
||||||
const creatorName = creatorDetails?.display_name ?? t("common.none");
|
const creatorName = creatorDetails?.display_name ?? t("common.none");
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
|
const cornerControlClasses =
|
||||||
const customActionButton = (
|
"flex h-12 w-12 items-center justify-center rounded-full border-0 bg-white text-[#0B1117] shadow-none outline-none ring-0 transition-transform hover:scale-[1.03]";
|
||||||
<div
|
const dateButton = (
|
||||||
data-control-link-ignore="true"
|
|
||||||
className={cn(
|
|
||||||
"flex h-8 w-8 cursor-pointer items-center justify-center rounded-full p-1 transition-colors",
|
|
||||||
isActive
|
|
||||||
? "bg-black text-[rgb(var(--nodedc-card-active-rgb))] hover:bg-black/90"
|
|
||||||
: "bg-[#111214] text-white hover:bg-[#0A0B0C]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const header = (
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<Avatar
|
|
||||||
src={getFileURL(creatorDetails?.avatar_url ?? "")}
|
|
||||||
name={creatorName}
|
|
||||||
size="md"
|
|
||||||
showTooltip={!isMobile}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="truncate text-body-sm-medium leading-5">{creatorName}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
|
||||||
{quickActions({
|
|
||||||
issue,
|
|
||||||
parentRef: cardRef,
|
|
||||||
customActionButton,
|
|
||||||
})}
|
|
||||||
<PriorityDropdown
|
|
||||||
value={issue.priority}
|
|
||||||
onChange={(priority) => updateIssue?.(issue.project_id ?? null, issue.id, { priority })}
|
|
||||||
disabled={isReadOnly || !updateIssue}
|
|
||||||
button={
|
|
||||||
<div
|
|
||||||
data-control-link-ignore="true"
|
|
||||||
className={cn(
|
|
||||||
"flex h-8 w-8 items-center justify-center rounded-full border-0 shadow-none outline-none",
|
|
||||||
iconBubbleClasses
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<PriorityIcon priority={issue.priority} className="h-3.5 w-3.5" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StateDropdown
|
|
||||||
projectId={issue.project_id ?? undefined}
|
|
||||||
stateIds={projectStateIds ?? []}
|
|
||||||
value={issue.state_id}
|
|
||||||
onChange={(stateId) => updateIssue?.(issue.project_id ?? null, issue.id, { state_id: stateId })}
|
|
||||||
disabled={isReadOnly || !updateIssue}
|
|
||||||
button={
|
|
||||||
<div
|
|
||||||
data-control-link-ignore="true"
|
|
||||||
className={cn("flex h-8 w-8 items-center justify-center rounded-full", iconBubbleClasses)}
|
|
||||||
>
|
|
||||||
<StateGroupIcon
|
|
||||||
stateGroup={selectedState?.group ?? "backlog"}
|
|
||||||
color={statusIconColor}
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
percentage={selectedState?.order}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const footer = (
|
|
||||||
<>
|
|
||||||
<MemberDropdown
|
|
||||||
projectId={issue.project_id ?? undefined}
|
|
||||||
value={issue.assignee_ids}
|
|
||||||
onChange={(assigneeIds) => updateIssue?.(issue.project_id ?? null, issue.id, { assignee_ids: assigneeIds })}
|
|
||||||
disabled={isReadOnly || !updateIssue}
|
|
||||||
button={
|
|
||||||
<div data-control-link-ignore="true" className={cn(basePillClasses, pillBackgroundClasses, "min-h-9 pl-1 pr-2")}>
|
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids} size="sm" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
|
className="h-auto self-start"
|
||||||
|
buttonContainerClassName="h-auto w-auto text-left"
|
||||||
value={issue.target_date}
|
value={issue.target_date}
|
||||||
rangePreview={{
|
rangePreview={{
|
||||||
from: issue.start_date,
|
from: issue.start_date,
|
||||||
|
|
@ -183,12 +94,115 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
}
|
}
|
||||||
disabled={isReadOnly || !updateIssue}
|
disabled={isReadOnly || !updateIssue}
|
||||||
button={
|
button={
|
||||||
<div data-control-link-ignore="true" className={cn(basePillClasses, pillBackgroundClasses)}>
|
<div
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
data-control-link-ignore="true"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex max-w-full items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] leading-4 font-medium",
|
||||||
|
pillBackgroundClasses
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarDays className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="truncate">{dueDateLabel}</span>
|
<span className="truncate">{dueDateLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const customActionButton = (
|
||||||
|
<div
|
||||||
|
data-control-link-ignore="true"
|
||||||
|
className={cn(
|
||||||
|
"flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-black text-white shadow-none ring-0 transition-transform outline-none hover:scale-[1.03]",
|
||||||
|
isActive
|
||||||
|
? "text-[rgb(var(--nodedc-card-active-rgb))] hover:bg-black/90"
|
||||||
|
: "bg-[#111214] text-white hover:bg-[#0A0B0C]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const header = (
|
||||||
|
<div className="relative z-10">
|
||||||
|
<div className="flex min-w-0 items-start gap-2.5 pt-3 pr-[96px] pl-3">
|
||||||
|
<div className="shrink-0">
|
||||||
|
<Avatar
|
||||||
|
src={getFileURL(creatorDetails?.avatar_url ?? "")}
|
||||||
|
name={creatorName}
|
||||||
|
size="md"
|
||||||
|
showTooltip={!isMobile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-col gap-1">
|
||||||
|
<div className="truncate text-body-sm-medium leading-5">{sourceContourName}</div>
|
||||||
|
{dateButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-0 right-0 flex shrink-0 items-center gap-0">
|
||||||
|
<PriorityDropdown
|
||||||
|
value={issue.priority}
|
||||||
|
onChange={(priority) => updateIssue?.(issue.project_id ?? null, issue.id, { priority })}
|
||||||
|
disabled={isReadOnly || !updateIssue}
|
||||||
|
button={
|
||||||
|
<div data-control-link-ignore="true" className={cornerControlClasses}>
|
||||||
|
<PriorityIcon priority={issue.priority} className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StateDropdown
|
||||||
|
projectId={issue.project_id ?? undefined}
|
||||||
|
stateIds={projectStateIds ?? []}
|
||||||
|
value={issue.state_id}
|
||||||
|
onChange={(stateId) => updateIssue?.(issue.project_id ?? null, issue.id, { state_id: stateId })}
|
||||||
|
disabled={isReadOnly || !updateIssue}
|
||||||
|
button={
|
||||||
|
<div data-control-link-ignore="true" className={cornerControlClasses}>
|
||||||
|
<StateGroupIcon
|
||||||
|
stateGroup={selectedState?.group ?? "backlog"}
|
||||||
|
color={statusIconColor}
|
||||||
|
className="h-4 w-4"
|
||||||
|
percentage={selectedState?.order}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<span>{issue.name}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const footer = (
|
||||||
|
<>
|
||||||
|
<MemberDropdown
|
||||||
|
projectId={issue.project_id ?? undefined}
|
||||||
|
value={issue.assignee_ids}
|
||||||
|
onChange={(assigneeIds) => updateIssue?.(issue.project_id ?? null, issue.id, { assignee_ids: assigneeIds })}
|
||||||
|
disabled={isReadOnly || !updateIssue}
|
||||||
|
multiple
|
||||||
|
className="h-11 w-11"
|
||||||
|
buttonContainerClassName="h-11 w-11"
|
||||||
|
button={
|
||||||
|
<div
|
||||||
|
data-control-link-ignore="true"
|
||||||
|
className={cn(basePillClasses, "h-11 min-w-11 justify-center rounded-full bg-transparent px-0 py-0")}
|
||||||
|
>
|
||||||
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids} size="sm" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
{quickActions({
|
||||||
|
issue,
|
||||||
|
parentRef: cardRef,
|
||||||
|
customActionButton,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -196,12 +210,14 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
return (
|
return (
|
||||||
<NodedcWorkItemCard
|
<NodedcWorkItemCard
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
surfaceClassName="px-0"
|
surfaceClassName="!rounded-[24px] !p-0"
|
||||||
contentClassName="px-1"
|
contentClassName="min-h-[220px]"
|
||||||
header={header}
|
header={header}
|
||||||
subtitle={sourceContourName}
|
title={title}
|
||||||
title={issue.name}
|
titleContainerClassName="px-6 pt-7 pb-14"
|
||||||
|
titleClassName="line-clamp-none w-full text-[15px] leading-5"
|
||||||
footer={footer}
|
footer={footer}
|
||||||
|
footerClassName="pointer-events-auto absolute inset-x-0 bottom-0 h-12 items-end gap-0"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,16 @@ export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "
|
||||||
export const STATE_GROUP_COLORS: {
|
export const STATE_GROUP_COLORS: {
|
||||||
[key in TStateGroups]: string;
|
[key in TStateGroups]: string;
|
||||||
} = {
|
} = {
|
||||||
backlog: "#60646C",
|
backlog: "#050505",
|
||||||
unstarted: "#60646C",
|
unstarted: "#7C7F85",
|
||||||
started: "#F59E0B",
|
started: "#050505",
|
||||||
completed: "#46A758",
|
completed: "rgb(var(--nodedc-card-active-rgb, 195 255 102))",
|
||||||
cancelled: "#9AA4BC",
|
cancelled: "#050505",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getStateGroupColor = (stateGroup: TStateGroups | undefined, fallbackColor?: string) =>
|
||||||
|
stateGroup ? STATE_GROUP_COLORS[stateGroup] : (fallbackColor ?? STATE_GROUP_COLORS.unstarted);
|
||||||
|
|
||||||
export const INTAKE_STATE_GROUP_COLORS: { [key in TIntakeStateGroups]: string } = { triage: "#4E5355" };
|
export const INTAKE_STATE_GROUP_COLORS: { [key in TIntakeStateGroups]: string } = { triage: "#4E5355" };
|
||||||
|
|
||||||
export const STATE_GROUP_SIZES: {
|
export const STATE_GROUP_SIZES: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue