[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:
Bavisetti Narayan
2024-09-30 19:54:38 +05:30
committed by GitHub
parent 7bfdcb9657
commit 5f584e06cd
7 changed files with 159 additions and 21 deletions

View File

@@ -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(

View File

@@ -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

View File

@@ -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,

View File

@@ -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}

View File

@@ -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>
);
});

View File

@@ -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>

View File

@@ -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 ?? []),