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