ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: восстановление профиля и локальной аналитики

This commit is contained in:
DCCONSTRUCTIONS 2026-04-26 12:56:50 +03:00
parent 8fde5e9502
commit 8b230d2670
8 changed files with 116 additions and 63 deletions

View File

@ -41,9 +41,10 @@ from plane.app.serializers import (
from plane.app.views.base import BaseAPIView
from plane.db.models import (
CycleIssue,
FileAsset,
IntakeIssue,
Issue,
IssueActivity,
FileAsset,
IssueLink,
IssueSubscriber,
Project,
@ -108,6 +109,18 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
)
)
.annotate(
created_by_display_name=F("created_by__display_name"),
created_by_avatar_url=F("created_by__avatar"),
)
.annotate(
source_project_name=Subquery(
IntakeIssue.objects.filter(
issue_id=OuterRef("id"),
extra__bridge="external-contours",
).values("extra__source_project_name")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()

View File

@ -8,15 +8,20 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import type { TProfileViews } from "@plane/types";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns";
import { BaseKanBanRoot } from "../base-kanban-root";
export const ProfileIssuesKanBanLayout = observer(function ProfileIssuesKanBanLayout() {
type Props = {
viewId: TProfileViews;
};
export const ProfileIssuesKanBanLayout = observer(function ProfileIssuesKanBanLayout({ viewId }: Props) {
// router
const { workspaceSlug, profileViewId } = useParams();
const { workspaceSlug } = useParams();
const { allowPermissions } = useUserPermissions();
const canEditPropertiesBasedOnProject = (projectId: string) =>
@ -31,7 +36,8 @@ export const ProfileIssuesKanBanLayout = observer(function ProfileIssuesKanBanLa
<BaseKanBanRoot
QuickActions={ProjectIssueQuickActions}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
viewId={profileViewId?.toString()}
cardVariant="internal-contour"
viewId={viewId}
/>
);
});

View File

@ -7,15 +7,20 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import type { TProfileViews } from "@plane/types";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns";
import { BaseListRoot } from "../base-list-root";
export const ProfileIssuesListLayout = observer(function ProfileIssuesListLayout() {
type Props = {
viewId: TProfileViews;
};
export const ProfileIssuesListLayout = observer(function ProfileIssuesListLayout({ viewId }: Props) {
// router
const { workspaceSlug, profileViewId } = useParams();
const { workspaceSlug } = useParams();
// store
const { allowPermissions } = useUserPermissions();
@ -31,7 +36,7 @@ export const ProfileIssuesListLayout = observer(function ProfileIssuesListLayout
<BaseListRoot
QuickActions={ProjectIssueQuickActions}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
viewId={profileViewId?.toString()}
viewId={viewId}
/>
);
});

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { TStateGroups } from "@plane/types";
export const PROFILE_STATE_GROUP_COLORS: Partial<Record<TStateGroups, string>> = {
backlog: "#050505",
unstarted: "#7C7F85",
started: "#F5F7FB",
completed: "#C3FF66",
cancelled: "#050505",
};
export const PROFILE_STATE_GROUP_LABELS: Partial<Record<TStateGroups, string>> = {
backlog: "Бэклог",
unstarted: "К выполнению",
started: "В работе",
completed: "Готово",
cancelled: "Отложено",
};
export const PROFILE_PRIORITY_COLORS: Record<string, string> = {
urgent: "#C3FF66",
high: "#F5F7FB",
medium: "#7C7F85",
low: "#2A2B2E",
none: "#050505",
};
export const PROFILE_PRIORITY_LABELS: Record<string, string> = {
urgent: "Срочно",
high: "Высокий",
medium: "Средний",
low: "Низкий",
none: "Нет",
};
export const getProfilePriorityKey = (priority: string | null | undefined) => priority?.trim().toLowerCase() || "none";
export const getProfilePriorityLabel = (priority: string | null | undefined) =>
PROFILE_PRIORITY_LABELS[getProfilePriorityKey(priority)] ?? priority ?? PROFILE_PRIORITY_LABELS.none;
export const getProfileStateGroupColor = (stateGroup: TStateGroups) =>
PROFILE_STATE_GROUP_COLORS[stateGroup] ?? "#7C7F85";
export const getProfileStateGroupLabel = (stateGroup: TStateGroups) =>
PROFILE_STATE_GROUP_LABELS[stateGroup] ?? stateGroup;

View File

@ -10,22 +10,25 @@ import { BarChart } from "@plane/propel/charts/bar-chart";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IUserProfileData } from "@plane/types";
import { Loader, Card } from "@plane/ui";
import { capitalizeFirstLetter } from "@plane/utils";
import { getProfilePriorityKey, getProfilePriorityLabel, PROFILE_PRIORITY_COLORS } from "./helpers";
type Props = {
userProfile: IUserProfileData | undefined;
};
const priorityColors = {
urgent: "#991b1b",
high: "#ef4444",
medium: "#f59e0b",
low: "#16a34a",
none: "#e5e5e5",
};
export function ProfilePriorityDistribution({ userProfile }: Props) {
const { t } = useTranslation();
const priorityChartData =
userProfile?.priority_distribution.map((priority) => {
const priorityKey = getProfilePriorityKey(priority.priority);
return {
key: priorityKey,
name: getProfilePriorityLabel(priorityKey),
count: priority.priority_count,
};
}) ?? [];
return (
<div className="flex flex-col space-y-2">
<h3 className="text-16 font-medium">{t("profile.stats.priority_distribution.title")}</h3>
@ -33,21 +36,18 @@ export function ProfilePriorityDistribution({ userProfile }: Props) {
<Card className="nodedc-workspace-list-row border-0 bg-transparent p-5">
{userProfile.priority_distribution.length > 0 ? (
<BarChart
className="h-[300px] w-full"
margin={{ top: 20, right: 30, bottom: 5, left: 0 }}
data={userProfile.priority_distribution.map((priority) => ({
key: priority.priority ?? "None",
name: capitalizeFirstLetter(priority.priority ?? "None"),
count: priority.priority_count,
}))}
className="nodedc-analytics-bar-chart h-[300px] w-full"
margin={{ top: 20, right: 22, bottom: 28, left: 8 }}
data={priorityChartData}
bars={[
{
key: "count",
label: "Count",
label: "Количество",
stackId: "bar-one",
fill: (payload: any) => priorityColors[payload.key as keyof typeof priorityColors], // TODO: fix types
fill: (payload: any) => PROFILE_PRIORITY_COLORS[payload.key] ?? PROFILE_PRIORITY_COLORS.none,
textClassName: "",
showPercentage: false,
borderRadius: 11,
showTopBorderRadius: () => true,
showBottomBorderRadius: () => true,
},
@ -60,7 +60,7 @@ export function ProfilePriorityDistribution({ userProfile }: Props) {
key: "count",
label: "",
}}
barSize={20}
barSize={86}
/>
) : (
<EmptyStateCompact

View File

@ -4,14 +4,12 @@
* See the LICENSE file for details.
*/
// plane imports
import { STATE_GROUPS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { PieChart } from "@plane/propel/charts/pie-chart";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IUserProfileData, IUserStateDistribution } from "@plane/types";
import { Card } from "@plane/ui";
import { capitalizeFirstLetter } from "@plane/utils";
import { getProfileStateGroupColor, getProfileStateGroupLabel } from "./helpers";
type Props = {
stateDistribution: IUserStateDistribution[];
@ -21,11 +19,6 @@ type Props = {
export function ProfileStateDistribution({ stateDistribution, userProfile }: Props) {
const { t } = useTranslation();
if (!userProfile) return null;
const stateGroupLabels = {
unstarted: t("yet_to_start"),
started: t("in_progress"),
completed: t("completed"),
} as const;
return (
<div className="flex flex-col space-y-2">
@ -47,21 +40,20 @@ export function ProfileStateDistribution({ stateDistribution, userProfile }: Pro
id: group.state_group,
key: group.state_group,
value: group.state_count,
name:
stateGroupLabels[group.state_group as keyof typeof stateGroupLabels] ??
capitalizeFirstLetter(group.state_group),
color: STATE_GROUPS[group.state_group]?.color,
name: getProfileStateGroupLabel(group.state_group),
color: getProfileStateGroupColor(group.state_group),
})) ?? []
}
cells={userProfile.state_distribution.map((group) => ({
key: group.state_group,
fill: STATE_GROUPS[group.state_group]?.color,
fill: getProfileStateGroupColor(group.state_group),
}))}
showTooltip
tooltipLabel="Count"
paddingAngle={5}
cornerRadius={4}
innerRadius="50%"
tooltipLabel="Количество"
paddingAngle={6}
cornerRadius={6}
innerRadius="42%"
outerRadius="84%"
showLabel={false}
/>
<div className="flex items-center">
@ -72,14 +64,10 @@ export function ProfileStateDistribution({ stateDistribution, userProfile }: Pro
<div
className="h-2.5 w-2.5 rounded-xs"
style={{
backgroundColor:
STATE_GROUPS[group.state_group]?.color ?? "var(--background-color-accent-primary)",
backgroundColor: getProfileStateGroupColor(group.state_group),
}}
/>
<div className="whitespace-nowrap">
{stateGroupLabels[group.state_group as keyof typeof stateGroupLabels] ??
STATE_GROUPS[group.state_group].label}
</div>
<div className="whitespace-nowrap">{getProfileStateGroupLabel(group.state_group)}</div>
</div>
<div>{group.state_count}</div>
</div>

View File

@ -4,12 +4,11 @@
* See the LICENSE file for details.
*/
// plane imports
import { STATE_GROUPS } from "@plane/constants";
// types
import { useTranslation } from "@plane/i18n";
import type { IUserStateDistribution } from "@plane/types";
import { Card, ECardDirection, ECardSpacing } from "@plane/ui";
import { getProfileStateGroupColor, getProfileStateGroupLabel } from "./helpers";
// constants
type Props = {
@ -18,11 +17,6 @@ type Props = {
export function ProfileWorkload({ stateDistribution }: Props) {
const { t } = useTranslation();
const stateGroupLabels = {
unstarted: t("yet_to_start"),
started: t("in_progress"),
completed: t("completed"),
} as const;
return (
<div className="space-y-2">
@ -39,14 +33,11 @@ export function ProfileWorkload({ stateDistribution }: Props) {
<div
className="my-2 h-3 w-3 rounded-xs"
style={{
backgroundColor: STATE_GROUPS[group.state_group].color,
backgroundColor: getProfileStateGroupColor(group.state_group),
}}
/>
<div className="flex-col space-y-1">
<span className="text-13 text-placeholder">
{stateGroupLabels[group.state_group as keyof typeof stateGroupLabels] ??
STATE_GROUPS[group.state_group].label}
</span>
<span className="text-13 text-placeholder">{getProfileStateGroupLabel(group.state_group)}</span>
<p className="text-18 font-semibold">{group.state_count}</p>
</div>
</Card>

View File

@ -66,9 +66,9 @@ export const ProfileIssuesPage = observer(function ProfileIssuesPage(props: Prop
{profileWorkItemsFilter && <WorkItemFiltersRow filter={profileWorkItemsFilter} />}
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProfileIssuesListLayout />
<ProfileIssuesListLayout viewId={type} />
) : activeLayout === "kanban" ? (
<ProfileIssuesKanBanLayout />
<ProfileIssuesKanBanLayout viewId={type} />
) : null}
</div>
</div>