mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 21:40:18 +02:00
[WEB-2568] chore: implement activity tracking for issue type changes (#1300)
* chore: issue type activity * chore: restrict issue type activity for normal users * chore: issue type activity implementation. * fix: issue with disabled issue type display. --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
committed by
GitHub
parent
7bfdcb9657
commit
5f584e06cd
@@ -29,6 +29,8 @@ from plane.db.models import (
|
||||
IssueComment,
|
||||
CommentReaction,
|
||||
)
|
||||
from plane.payment.flags.flag_decorator import check_workspace_feature_flag
|
||||
from plane.payment.flags.flag import FeatureFlag
|
||||
|
||||
|
||||
class IssueActivityEndpoint(BaseAPIView):
|
||||
@@ -61,6 +63,13 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
.filter(**filters)
|
||||
.select_related("actor", "workspace", "issue", "project")
|
||||
).order_by("created_at")
|
||||
|
||||
if not check_workspace_feature_flag(
|
||||
feature_key=FeatureFlag.ISSUE_TYPE_SETTINGS,
|
||||
slug=slug,
|
||||
):
|
||||
issue_activities = issue_activities.filter(field="type")
|
||||
|
||||
issue_comments = (
|
||||
IssueComment.objects.filter(issue_id=issue_id)
|
||||
.filter(
|
||||
|
||||
@@ -27,6 +27,7 @@ from plane.db.models import (
|
||||
State,
|
||||
User,
|
||||
EstimatePoint,
|
||||
IssueType,
|
||||
)
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
@@ -570,6 +571,51 @@ def track_closed_to(
|
||||
)
|
||||
|
||||
|
||||
def track_type(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
|
||||
new_type_id = requested_data.get("type_id")
|
||||
old_type_id = current_instance.get("type_id")
|
||||
|
||||
if new_type_id != old_type_id:
|
||||
verb = "updated" if new_type_id else "deleted"
|
||||
old_type_id = (
|
||||
IssueType.objects.filter(pk=old_type_id).first()
|
||||
if old_type_id
|
||||
else None
|
||||
)
|
||||
new_type_id = (
|
||||
IssueType.objects.filter(pk=new_type_id).first()
|
||||
if new_type_id
|
||||
else None
|
||||
)
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb=verb,
|
||||
old_value=old_type_id.name if old_type_id else None,
|
||||
new_value=new_type_id.name if new_type_id else None,
|
||||
field="type",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="",
|
||||
old_identifier=old_type_id.id if old_type_id else None,
|
||||
new_identifier=new_type_id.id if new_type_id else None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
@@ -632,6 +678,7 @@ def update_issue_activity(
|
||||
"estimate_point": track_estimate_points,
|
||||
"archived_at": track_archive_at,
|
||||
"closed_to": track_closed_to,
|
||||
"type_id": track_type,
|
||||
}
|
||||
|
||||
requested_data = (
|
||||
@@ -1700,16 +1747,12 @@ def issue_activity(
|
||||
event=(
|
||||
"issue_comment"
|
||||
if activity.field == "comment"
|
||||
else "inbox_issue"
|
||||
if inbox
|
||||
else "issue"
|
||||
else "inbox_issue" if inbox else "issue"
|
||||
),
|
||||
event_id=(
|
||||
activity.issue_comment_id
|
||||
if activity.field == "comment"
|
||||
else inbox
|
||||
if inbox
|
||||
else activity.issue_id
|
||||
else inbox if inbox else activity.issue_id
|
||||
),
|
||||
verb=activity.verb,
|
||||
field=(
|
||||
@@ -1755,4 +1798,3 @@ def issue_activity(
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LayersIcon, LUCIDE_ICONS_LIST } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
export type TIssueTypeLogoSize = "sm" | "md" | "lg" | "xl";
|
||||
export type TIssueTypeLogoSize = "xs" | "sm" | "md" | "lg" | "xl";
|
||||
|
||||
type Props = {
|
||||
icon_props: TLogoProps["icon"];
|
||||
@@ -16,6 +16,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const iconSizeMap = {
|
||||
xs: 11,
|
||||
sm: 12.5,
|
||||
md: 14.5,
|
||||
lg: 18,
|
||||
@@ -23,6 +24,7 @@ const iconSizeMap = {
|
||||
};
|
||||
|
||||
const containerSizeMap = {
|
||||
xs: 15.5,
|
||||
sm: 18,
|
||||
md: 21,
|
||||
lg: 25.5,
|
||||
|
||||
@@ -18,18 +18,16 @@ export const IssueTypeDropdown = observer((props: TIssueTypeDropdownProps) => {
|
||||
// store hooks
|
||||
const { loader: issueTypesLoader, getProjectIssueTypes } = useIssueTypes();
|
||||
// derived values
|
||||
const issueTypes = getProjectIssueTypes(projectId, true);
|
||||
const allIssueTypes = getProjectIssueTypes(projectId, false);
|
||||
const activeIssueTypes = getProjectIssueTypes(projectId, true);
|
||||
|
||||
// Can be used with CustomSearchSelect as well
|
||||
const issueTypeOptions = Object.entries(issueTypes).map(([issueTypeId, issueTypeDetail]) => ({
|
||||
const issueTypeOptions = Object.entries(activeIssueTypes).map(([issueTypeId, issueTypeDetail]) => ({
|
||||
value: issueTypeId,
|
||||
query: issueTypeDetail.name ?? "",
|
||||
content: (
|
||||
<div className="flex w-full gap-2 items-center">
|
||||
<IssueTypeLogo
|
||||
icon_props={issueTypeDetail?.logo_props?.icon}
|
||||
isDefault={issueTypeDetail?.is_default}
|
||||
/>
|
||||
<IssueTypeLogo icon_props={issueTypeDetail?.logo_props?.icon} isDefault={issueTypeDetail?.is_default} />
|
||||
<div className="text-sm font-medium text-custom-text-200 truncate">{issueTypeDetail.name}</div>
|
||||
</div>
|
||||
),
|
||||
@@ -49,10 +47,10 @@ export const IssueTypeDropdown = observer((props: TIssueTypeDropdownProps) => {
|
||||
label={
|
||||
<div className="flex w-full gap-2 items-center max-w-44">
|
||||
<IssueTypeLogo
|
||||
icon_props={issueTypes[issueTypeId]?.logo_props?.icon}
|
||||
isDefault={issueTypes[issueTypeId]?.is_default}
|
||||
icon_props={allIssueTypes[issueTypeId]?.logo_props?.icon}
|
||||
isDefault={allIssueTypes[issueTypeId]?.is_default}
|
||||
/>
|
||||
<div className="text-sm font-medium text-custom-text-200 truncate">{issueTypes[issueTypeId]?.name}</div>
|
||||
<div className="text-sm font-medium text-custom-text-200 truncate">{allIssueTypes[issueTypeId]?.name}</div>
|
||||
</div>
|
||||
}
|
||||
options={issueTypeOptions}
|
||||
|
||||
@@ -1 +1,79 @@
|
||||
export * from "ce/components/issues/issue-details";
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ArrowRightLeft } from "lucide-react";
|
||||
// ce imports
|
||||
import { IssueTypeActivity as BaseIssueTypeActivity, TIssueTypeActivity } from "@/ce/components/issues";
|
||||
// components
|
||||
import {
|
||||
IssueActivityBlockComponent,
|
||||
IssueLink,
|
||||
} from "@/components/issues/issue-detail/issue-activity/activity/actions";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueTypeLogo } from "@/plane-web/components/issue-types";
|
||||
// plane web hooks
|
||||
import { useIssueTypes } from "@/plane-web/hooks/store";
|
||||
|
||||
type TIssueTypeDetail = {
|
||||
issueTypeId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const IssueTypeDetail: FC<TIssueTypeDetail> = observer((props) => {
|
||||
const { issueTypeId, className = "" } = props;
|
||||
// hooks
|
||||
const { getIssueTypeById } = useIssueTypes();
|
||||
// derived values
|
||||
const issueTypeDetail = getIssueTypeById(issueTypeId);
|
||||
|
||||
return (
|
||||
<span className={cn("inline-flex gap-1 items-center font-medium text-custom-text-100", className)}>
|
||||
<IssueTypeLogo icon_props={issueTypeDetail?.logo_props?.icon} size="xs" isDefault={issueTypeDetail?.is_default} />
|
||||
{issueTypeDetail?.name}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
export const IssueTypeActivity: FC<TIssueTypeActivity> = observer((props) => {
|
||||
const { activityId, showIssue = false, ends } = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
const { isIssueTypeEnabledForProject } = useIssueTypes();
|
||||
// derived values
|
||||
const activity = getActivityById(activityId);
|
||||
if (!activity) return <></>;
|
||||
|
||||
const isIssueTypeDisplayEnabled =
|
||||
workspaceSlug && activity?.project
|
||||
? isIssueTypeEnabledForProject(workspaceSlug?.toString(), activity?.project, "ISSUE_TYPE_DISPLAY")
|
||||
: false;
|
||||
|
||||
if (!isIssueTypeDisplayEnabled) return <BaseIssueTypeActivity {...props} />;
|
||||
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<ArrowRightLeft className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-200" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
<span className="inline-flex items-center">
|
||||
changed issue type to{" "}
|
||||
{activity.new_identifier && <IssueTypeDetail issueTypeId={activity.new_identifier} className="px-1" />}
|
||||
from {activity.old_identifier && <IssueTypeDetail issueTypeId={activity.old_identifier} className="pl-1" />}
|
||||
{showIssue ? ` for ` : ``}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</span>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ export const IssueTypeSwitcher: React.FC<TIssueTypeSwitcherProps> = observer((pr
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
toggleCreateIssueModal,
|
||||
fetchActivities,
|
||||
} = useIssueDetail();
|
||||
const { isIssueTypeEnabledForProject } = useIssueTypes();
|
||||
// derived values
|
||||
@@ -54,6 +55,11 @@ export const IssueTypeSwitcher: React.FC<TIssueTypeSwitcherProps> = observer((pr
|
||||
<>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isCreateUpdateIssueModalOpen}
|
||||
onSubmit={async () => {
|
||||
if (workspaceSlug && issue.project_id) {
|
||||
await fetchActivities(workspaceSlug.toString(), issue.project_id, issueId);
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsCreateUpdateIssueModalOpen(false);
|
||||
toggleCreateIssueModal(false);
|
||||
@@ -72,7 +78,7 @@ export const IssueTypeSwitcher: React.FC<TIssueTypeSwitcherProps> = observer((pr
|
||||
{!disabled && (
|
||||
<span className="flex opacity-0 group-hover:opacity-100 transition-opacity duration-300 items-center gap-1 text-xs font-medium text-custom-text-300">
|
||||
<ArrowRightLeft className="w-3 h-3 flex-shrink-0" />
|
||||
Switch Issue Type
|
||||
Switch issue type
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useIssueDetail } from "@/hooks/store";
|
||||
// plane web helpers
|
||||
import { getPropertiesDefaultValues } from "@/plane-web/helpers/issue-properties.helper";
|
||||
// plane web hooks
|
||||
import { useIssueTypes } from "@/plane-web/hooks/store";
|
||||
import { useIssuePropertiesActivity, useIssueTypes } from "@/plane-web/hooks/store";
|
||||
// plane web services
|
||||
import { IssuePropertyValuesService } from "@/plane-web/services/issue-types";
|
||||
// plane web types
|
||||
@@ -44,7 +44,7 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||
getProjectIssueTypes,
|
||||
getProjectDefaultIssueType,
|
||||
} = useIssueTypes();
|
||||
|
||||
const { fetchPropertyActivities } = useIssuePropertiesActivity();
|
||||
// helpers
|
||||
const getIssueTypeIdOnProjectChange = (projectId: string) => {
|
||||
// get active issue types for the project
|
||||
@@ -135,7 +135,10 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||
await issuePropertyValuesService
|
||||
.create(workspaceSlug, projectId, issueId, filteredIssuePropertyValues)
|
||||
.then(() => {
|
||||
// mutate issue property values
|
||||
mutate(`ISSUE_PROPERTY_VALUES_${workspaceSlug}_${projectId}_${issueId}_${isIssueTypeDisplayEnabled}`);
|
||||
// fetch property activities
|
||||
fetchPropertyActivities(workspaceSlug, projectId, issueId);
|
||||
// reset issue property values
|
||||
setIssuePropertyValues({
|
||||
...getPropertiesDefaultValues(issueType?.activeProperties ?? []),
|
||||
|
||||
Reference in New Issue
Block a user