ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: стабилизация контуров, досок и Voice Tasker

This commit is contained in:
DCCONSTRUCTIONS
2026-04-24 23:34:20 +03:00
parent 597480adb9
commit 7209d2caab
37 changed files with 960 additions and 390 deletions
@@ -10,14 +10,9 @@ import { CalendarDays } from "lucide-react";
import { observer } from "mobx-react";
import { EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { PriorityIcon, StateGroupIcon } from "@plane/propel/icons";
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 type { IState, TExternalContourBoardDirection, TExternalContourRequest, TIssue } from "@plane/types";
import { Avatar } from "@plane/ui";
import { cn, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
import { DateDropdown } from "@/components/dropdowns/date";
@@ -56,7 +51,7 @@ const buildSourceStateMap = (
state.id,
{
id: state.id,
color: state.color,
color: getStateGroupColor(state.group, state.color),
default: false,
description: "",
group: state.group,
@@ -69,7 +64,10 @@ const buildSourceStateMap = (
])
);
const resolveRequestStatus = (issue: TExternalContourRequest["issue"], fallbackStatus: TExternalContourRequest["status"]) => {
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";
@@ -83,23 +81,20 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
const { getUserDetails, workspace } = useMember();
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
const { getStateById, getProjectStateIds } = useProjectState();
const {
fetchBoard,
upsertBoardItems,
} = useProjectExternalContoursBoard();
const {
fetchTargetOptions,
getTargetOptionsByProjectId,
updateRequest,
updateRequestIssue,
} = useProjectExternalContours();
const { fetchBoard, upsertBoardItems } = useProjectExternalContoursBoard();
const { fetchTargetOptions, getTargetOptionsByProjectId, updateRequest, updateRequestIssue } =
useProjectExternalContours();
const [isUpdating, setIsUpdating] = useState(false);
const [isSourceOptionsLoading, setIsSourceOptionsLoading] = 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 requester =
request.requested_by?.display_name ||
request.requested_by_name ||
issue.created_by_detail?.display_name ||
"NODE.DC";
const requesterAvatar = issue.created_by_detail?.avatar_url || "";
const counterpartContourName =
direction === "outgoing"
@@ -110,8 +105,12 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
? getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId)
: undefined;
const canEditTargetIssue =
direction === "incoming" && !!targetProjectId && projectRole !== undefined && projectRole !== EUserPermissions.GUEST;
const canEditSourceRequest = direction === "outgoing" && !!request.capabilities?.can_edit_request && !!targetProjectId;
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);
@@ -124,12 +123,11 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white";
const subtleTextClasses = isActive ? "text-[#2F4721]" : "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 pillBackgroundClasses = isActive
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
: "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";
const iconBubbleClasses = isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white";
const statusIconColor = selectedState?.color ?? (isActive ? "rgb(var(--nodedc-on-card-active-rgb))" : "var(--text-color-primary)");
const statusIconColor = getStateGroupColor(selectedState?.group, selectedState?.color);
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
if (!issue) return null;
@@ -314,13 +312,13 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
</div>
</div>
<div className={cn("truncate -mt-0.5 pl-8 text-[11px] font-medium leading-4", subtleTextClasses)}>
<div className={cn("-mt-0.5 truncate pl-8 text-[11px] leading-4 font-medium", subtleTextClasses)}>
{counterpartContourName || t("common.none")}
</div>
</div>
<div className="flex flex-1 items-center justify-center px-5 py-4 text-center">
<div className="line-clamp-4 max-w-full text-lg font-semibold leading-6">{issue.name}</div>
<div className="text-lg line-clamp-4 max-w-full leading-6 font-semibold">{issue.name}</div>
</div>
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
@@ -333,7 +331,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
disabled={!canEditCard || isUpdating}
buttonVariant="transparent-without-text"
button={
<div className={cn(basePillClasses, pillBackgroundClasses, "pl-1 pr-2")}>
<div className={cn(basePillClasses, pillBackgroundClasses, "pr-2 pl-1")}>
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
</div>
}
@@ -351,7 +349,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
}}
buttonVariant="transparent-without-text"
button={
<div className={cn(basePillClasses, pillBackgroundClasses, "pl-1 pr-2")}>
<div className={cn(basePillClasses, pillBackgroundClasses, "pr-2 pl-1")}>
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
</div>
}
@@ -17,6 +17,7 @@ import {
StateGroupIcon,
StatePropertyIcon,
UserCirclePropertyIcon,
getStateGroupColor,
} from "@plane/propel/icons";
import type {
IProject,
@@ -57,14 +58,11 @@ const buildCounterpartyProjects = (requests: TExternalContourRequest[], projectI
if (!project?.id || project.id === projectId || projectMap.has(project.id)) return;
projectMap.set(
project.id,
{
id: project.id,
name: project.name,
logo_props: project.logo_props,
} as IProject
);
projectMap.set(project.id, {
id: project.id,
name: project.name,
logo_props: project.logo_props,
} as IProject);
});
return sortByName(Array.from(projectMap.values()));
@@ -77,21 +75,18 @@ const buildStates = (requests: TExternalContourRequest[]): IState[] => {
const state = request.issue.state_detail;
if (!state?.id || stateMap.has(state.id)) return;
stateMap.set(
state.id,
{
id: state.id,
color: state.color,
default: false,
description: "",
group: state.group,
name: state.name,
order: index + 1,
project_id: request.issue.project_id || request.target_project?.id || request.source_project?.id || "",
sequence: index + 1,
workspace_id: "",
} as IState
);
stateMap.set(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: request.issue.project_id || request.target_project?.id || request.source_project?.id || "",
sequence: index + 1,
workspace_id: "",
} as IState);
});
return sortByName(Array.from(stateMap.values()));
@@ -103,18 +98,15 @@ const buildLabels = (requests: TExternalContourRequest[]): IIssueLabel[] => {
requests.forEach((request) => {
request.issue.label_details?.forEach((label) => {
if (!label.id || labelMap.has(label.id)) return;
labelMap.set(
label.id,
{
id: label.id,
color: label.color,
name: label.name,
parent: null,
project_id: request.issue.project_id || "",
sort_order: 0,
workspace_id: "",
} as IIssueLabel
);
labelMap.set(label.id, {
id: label.id,
color: label.color,
name: label.name,
parent: null,
project_id: request.issue.project_id || "",
sort_order: 0,
workspace_id: "",
} as IIssueLabel);
});
});
@@ -127,14 +119,11 @@ const buildAssignees = (requests: TExternalContourRequest[]): IUserLite[] => {
requests.forEach((request) => {
request.issue.assignee_details?.forEach((assignee) => {
if (!assignee.id || memberMap.has(assignee.id)) return;
memberMap.set(
assignee.id,
{
id: assignee.id,
avatar_url: assignee.avatar_url,
display_name: assignee.display_name,
} as IUserLite
);
memberMap.set(assignee.id, {
id: assignee.id,
avatar_url: assignee.avatar_url,
display_name: assignee.display_name,
} as IUserLite);
});
});
@@ -151,14 +140,11 @@ const buildRequesters = (requests: TExternalContourRequest[]): IUserLite[] => {
if (!requesterId || !requesterName || memberMap.has(requesterId)) return;
memberMap.set(
requesterId,
{
id: requesterId,
avatar_url: request.issue.created_by_detail?.avatar_url,
display_name: requesterName,
} as IUserLite
);
memberMap.set(requesterId, {
id: requesterId,
avatar_url: request.issue.created_by_detail?.avatar_url,
display_name: requesterName,
} as IUserLite);
});
return sortByName(Array.from(memberMap.values()));