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

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.app.views.base import BaseAPIView
from plane.db.models import ( from plane.db.models import (
CycleIssue, CycleIssue,
FileAsset,
IntakeIssue,
Issue, Issue,
IssueActivity, IssueActivity,
FileAsset,
IssueLink, IssueLink,
IssueSubscriber, IssueSubscriber,
Project, Project,
@ -108,6 +109,18 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1] 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( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by() .order_by()

View File

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

View File

@ -7,15 +7,20 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import type { TProfileViews } from "@plane/types";
// hooks // hooks
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
// local imports // local imports
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns";
import { BaseListRoot } from "../base-list-root"; 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 // router
const { workspaceSlug, profileViewId } = useParams(); const { workspaceSlug } = useParams();
// store // store
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
@ -31,7 +36,7 @@ export const ProfileIssuesListLayout = observer(function ProfileIssuesListLayout
<BaseListRoot <BaseListRoot
QuickActions={ProjectIssueQuickActions} QuickActions={ProjectIssueQuickActions}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject} 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 { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IUserProfileData } from "@plane/types"; import type { IUserProfileData } from "@plane/types";
import { Loader, Card } from "@plane/ui"; import { Loader, Card } from "@plane/ui";
import { capitalizeFirstLetter } from "@plane/utils"; import { getProfilePriorityKey, getProfilePriorityLabel, PROFILE_PRIORITY_COLORS } from "./helpers";
type Props = { type Props = {
userProfile: IUserProfileData | undefined; userProfile: IUserProfileData | undefined;
}; };
const priorityColors = {
urgent: "#991b1b",
high: "#ef4444",
medium: "#f59e0b",
low: "#16a34a",
none: "#e5e5e5",
};
export function ProfilePriorityDistribution({ userProfile }: Props) { export function ProfilePriorityDistribution({ userProfile }: Props) {
const { t } = useTranslation(); 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 ( return (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<h3 className="text-16 font-medium">{t("profile.stats.priority_distribution.title")}</h3> <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"> <Card className="nodedc-workspace-list-row border-0 bg-transparent p-5">
{userProfile.priority_distribution.length > 0 ? ( {userProfile.priority_distribution.length > 0 ? (
<BarChart <BarChart
className="h-[300px] w-full" className="nodedc-analytics-bar-chart h-[300px] w-full"
margin={{ top: 20, right: 30, bottom: 5, left: 0 }} margin={{ top: 20, right: 22, bottom: 28, left: 8 }}
data={userProfile.priority_distribution.map((priority) => ({ data={priorityChartData}
key: priority.priority ?? "None",
name: capitalizeFirstLetter(priority.priority ?? "None"),
count: priority.priority_count,
}))}
bars={[ bars={[
{ {
key: "count", key: "count",
label: "Count", label: "Количество",
stackId: "bar-one", 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: "", textClassName: "",
showPercentage: false, showPercentage: false,
borderRadius: 11,
showTopBorderRadius: () => true, showTopBorderRadius: () => true,
showBottomBorderRadius: () => true, showBottomBorderRadius: () => true,
}, },
@ -60,7 +60,7 @@ export function ProfilePriorityDistribution({ userProfile }: Props) {
key: "count", key: "count",
label: "", label: "",
}} }}
barSize={20} barSize={86}
/> />
) : ( ) : (
<EmptyStateCompact <EmptyStateCompact

View File

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

View File

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

View File

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