ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: восстановление профиля и локальной аналитики
This commit is contained in:
parent
8fde5e9502
commit
8b230d2670
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue