mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 13:29:56 +02:00
[WEB-2195] dev: move issue types to workspace level (#852)
* chore: move issues types to workspace * fix: add project id to issue type get endpoint. * dev: multi project issue types * chore: update issue types to support multiple project. * dev: update default project issue type * fix: issue type activity * chore: order issue types by `created_at` date. * dev: add filter by issue types. --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
@@ -147,7 +147,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
if not issue_type:
|
||||
# Get default issue type
|
||||
issue_type = IssueType.objects.filter(
|
||||
project_id=project_id, is_default=True
|
||||
project_issue_types__project_id=project_id, is_default=True
|
||||
).first()
|
||||
issue_type = issue_type
|
||||
|
||||
|
||||
@@ -109,6 +109,6 @@ from .dashboard import Dashboard, DashboardWidget, Widget
|
||||
|
||||
from .favorite import UserFavorite
|
||||
|
||||
from .issue_type import IssueType
|
||||
from .issue_type import IssueType, ProjectIssueType
|
||||
|
||||
from .recent_visit import UserRecentVisit
|
||||
from .recent_visit import UserRecentVisit
|
||||
|
||||
@@ -13,6 +13,10 @@ from plane.ee.models import (
|
||||
|
||||
class IssueTypeSerializer(BaseSerializer):
|
||||
issue_exists = serializers.BooleanField(read_only=True)
|
||||
project_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueType
|
||||
@@ -34,7 +38,6 @@ class IssuePropertySerializer(BaseSerializer):
|
||||
"name",
|
||||
"issue_type",
|
||||
"workspace",
|
||||
"project",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
@@ -47,7 +50,6 @@ class IssuePropertyOptionSerializer(BaseSerializer):
|
||||
read_only_fields = [
|
||||
"property",
|
||||
"workspace",
|
||||
"project",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Django imports
|
||||
from django.db.models import Exists, OuterRef
|
||||
from django.db.models import Exists, OuterRef, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -7,7 +9,7 @@ from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.ee.views.base import BaseAPIView
|
||||
from plane.db.models import IssueType, Issue, Project
|
||||
from plane.db.models import IssueType, Issue, Project, ProjectIssueType
|
||||
from plane.ee.permissions import (
|
||||
ProjectEntityPermission,
|
||||
WorkspaceEntityPermission,
|
||||
@@ -25,17 +27,35 @@ class WorkspaceIssueTypeEndpoint(BaseAPIView):
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_DISPLAY)
|
||||
def get(self, request, slug):
|
||||
# Get all issue types for the workspace
|
||||
issue_types = IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
).annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug, type_id=OuterRef("pk")
|
||||
issue_types = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project__project_projectmember__member=request.user,
|
||||
project_issue_types__project__project_projectmember__is_active=True,
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
).order_by("created_at")
|
||||
serializer = IssueTypeSerializer(issue_types, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -54,44 +74,111 @@ class IssueTypeEndpoint(BaseAPIView):
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(project_id=project_id, type_id=pk)
|
||||
)
|
||||
).get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
).get(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=pk,
|
||||
)
|
||||
serializer = IssueTypeSerializer(issue_type)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# Get all issue types
|
||||
issue_types = IssueType.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
issue_types = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
).order_by("created_at")
|
||||
|
||||
serializer = IssueTypeSerializer(issue_types, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
|
||||
# @check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
|
||||
def post(self, request, slug, project_id):
|
||||
# Fetch the project
|
||||
project = Project.objects.get(pk=project_id)
|
||||
# Create a new issue type
|
||||
serializer = IssueTypeSerializer(data=request.data)
|
||||
# check weight
|
||||
if not request.data.get("weight"):
|
||||
request.data["weight"] = 1
|
||||
# Check is_active
|
||||
if not request.data.get("is_active"):
|
||||
request.data["is_active"] = False
|
||||
# Validate the data
|
||||
serializer.is_valid(raise_exception=True)
|
||||
# Save the data
|
||||
serializer.save(project_id=project_id)
|
||||
serializer.save(workspace_id=project.workspace_id)
|
||||
|
||||
# Bridge the issue type with the project
|
||||
ProjectIssueType.objects.create(
|
||||
project_id=project_id,
|
||||
issue_type_id=serializer.data["id"],
|
||||
level=0,
|
||||
)
|
||||
|
||||
# Refetch the data
|
||||
issue_type = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=serializer.data["id"],
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
# Serialize the data
|
||||
serializer = IssueTypeSerializer(issue_type)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
# Update an issue type
|
||||
issue_type = IssueType.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
# Default cannot be made in active
|
||||
@@ -110,13 +197,50 @@ class IssueTypeEndpoint(BaseAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
# Save the data
|
||||
serializer.save()
|
||||
|
||||
# Refetch the data
|
||||
issue_type = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=serializer.data["id"],
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Serialize the data
|
||||
serializer = IssueTypeSerializer(issue_type.first())
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
# Delete an issue type
|
||||
issue_type = IssueType.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
# Check if there are any issues using this issue type
|
||||
@@ -150,32 +274,39 @@ class DefaultIssueTypeEndpoint(BaseAPIView):
|
||||
|
||||
# If issue type is already created return an error
|
||||
if IssueType.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
is_default=True,
|
||||
).exists():
|
||||
return Response(
|
||||
{{"error": "Default issue type already exists"}},
|
||||
{"error": "Default issue type already exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if default issue type exists for the project
|
||||
if ProjectIssueType.objects.filter(
|
||||
project_id=project_id, is_default=True
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "Default issue type already exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Create a new default issue type
|
||||
issue_type, _ = IssueType.objects.get_or_create(
|
||||
project_id=project_id,
|
||||
issue_type = IssueType.objects.create(
|
||||
workspace_id=project.workspace_id,
|
||||
name="Issue",
|
||||
is_default=True,
|
||||
defaults={
|
||||
"description": "Default issue type with the option to add new properties",
|
||||
"is_default": True,
|
||||
"weight": 0,
|
||||
"sort_order": 1,
|
||||
"logo_props": {
|
||||
"in_use": "icon",
|
||||
"icon": {
|
||||
"color": "#ffffff",
|
||||
"background_color": "#6695FF",
|
||||
},
|
||||
description="Default issue type with the option to add new properties",
|
||||
logo_props={
|
||||
"in_use": "icon",
|
||||
"icon": {
|
||||
"color": "#ffffff",
|
||||
"background_color": "#6695FF",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Update existing issues to use the new default issue type
|
||||
Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
@@ -187,5 +318,46 @@ class DefaultIssueTypeEndpoint(BaseAPIView):
|
||||
project.is_issue_type_enabled = True
|
||||
project.save()
|
||||
|
||||
# Bridge the issue type with the project
|
||||
ProjectIssueType.objects.create(
|
||||
project_id=project_id,
|
||||
issue_type_id=issue_type.id,
|
||||
level=0,
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
# Refetch the data
|
||||
issue_type = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=issue_type.id,
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Serialize the data
|
||||
return Response(str(issue_type.id), status=status.HTTP_201_CREATED)
|
||||
serializer = IssueTypeSerializer(issue_type.first())
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -230,9 +230,16 @@ class IssuePropertyValueEndpoint(BaseAPIView):
|
||||
existing_prop_queryset
|
||||
).values("property_id", "values")
|
||||
|
||||
# existing values
|
||||
existing_values = {
|
||||
str(prop["property_id"]): prop["values"]
|
||||
for prop in existing_prop_values
|
||||
}
|
||||
|
||||
# Get the value
|
||||
values = request.data.get("values", [])
|
||||
|
||||
# Check if the property is required
|
||||
if issue_property.is_required and (
|
||||
not values or not [v for v in values if v]
|
||||
):
|
||||
@@ -277,10 +284,7 @@ class IssuePropertyValueEndpoint(BaseAPIView):
|
||||
|
||||
# Log the activity
|
||||
issue_property_activity.delay(
|
||||
existing_values={
|
||||
str(prop["property_id"]): prop["values"]
|
||||
for prop in existing_prop_values
|
||||
},
|
||||
existing_values=existing_values,
|
||||
requested_values={str(property_id): values},
|
||||
issue_id=issue_id,
|
||||
user_id=str(request.user.id),
|
||||
|
||||
@@ -524,6 +524,22 @@ def filter_logged_by(params, issue_filter, method, prefix=""):
|
||||
return issue_filter
|
||||
|
||||
|
||||
def filter_issue_type(params, issue_filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
types = [item for item in params.get("issue_type").split(",") if item != "null"]
|
||||
types = filter_valid_uuids(types)
|
||||
if len(types) and "" not in types:
|
||||
issue_filter[f"{prefix}type__in"] = types
|
||||
else:
|
||||
if (
|
||||
params.get("issue_type", None)
|
||||
and len(params.get("issue_type"))
|
||||
and params.get("issue_type") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}type__in"] = params.get("issue_type")
|
||||
return issue_filter
|
||||
|
||||
|
||||
def issue_filters(query_params, method, prefix=""):
|
||||
issue_filter = {}
|
||||
|
||||
@@ -552,6 +568,7 @@ def issue_filters(query_params, method, prefix=""):
|
||||
"sub_issue": filter_sub_issue_toggle,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"start_target_date": filter_start_target_date_issues,
|
||||
"issue_type": filter_issue_type,
|
||||
}
|
||||
|
||||
for key, value in ISSUE_FILTER.items():
|
||||
|
||||
@@ -16,9 +16,9 @@ type TIssueTypeDropdownProps = {
|
||||
export const IssueTypeDropdown = observer((props: TIssueTypeDropdownProps) => {
|
||||
const { issueTypeId, projectId, disabled = false, handleIssueTypeChange } = props;
|
||||
// store hooks
|
||||
const { loader: issueTypesLoader, getProjectActiveIssueTypes } = useIssueTypes();
|
||||
const { loader: issueTypesLoader, getProjectIssueTypes } = useIssueTypes();
|
||||
// derived values
|
||||
const issueTypes = getProjectActiveIssueTypes(projectId);
|
||||
const issueTypes = getProjectIssueTypes(projectId, true);
|
||||
|
||||
// Can be used with CustomSearchSelect as well
|
||||
const issueTypeOptions = Object.entries(issueTypes).map(([issueTypeId, issueTypeDetail]) => ({
|
||||
|
||||
@@ -1 +1,63 @@
|
||||
export * from "ce/components/issues/filters/applied-filters/issue-types";
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { X } from "lucide-react";
|
||||
// plane web components
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { IssueTypeLogo } from "@/plane-web/components/issue-types";
|
||||
// plane web hooks
|
||||
import { useFlag, useIssueTypes } from "@/plane-web/hooks/store";
|
||||
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedIssueTypeFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, values, editable } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { getIssueTypeById } = useIssueTypes();
|
||||
const isIssueTypeDisplayEnabled = useFlag(workspaceSlug?.toString(), "ISSUE_TYPE_DISPLAY");
|
||||
// derived values
|
||||
const projectDetails = getProjectById(projectId?.toString());
|
||||
|
||||
// Return null if issue type is not enabled for the project
|
||||
if (!isIssueTypeDisplayEnabled || (projectId && !projectDetails?.is_issue_type_enabled)) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{values.map((issueTypeId) => {
|
||||
const issueType = getIssueTypeById(issueTypeId);
|
||||
if (!issueType) return null;
|
||||
return (
|
||||
<div
|
||||
key={issueTypeId}
|
||||
className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs truncate"
|
||||
>
|
||||
<IssueTypeLogo
|
||||
icon_props={issueType?.logo_props?.icon}
|
||||
size={10}
|
||||
containerSize={16}
|
||||
isDefault={issueType?.is_default}
|
||||
/>
|
||||
<span className="normal-case truncate">{issueType.name}</span>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(issueTypeId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1 +1,118 @@
|
||||
export * from "ce/components/issues/filters/issue-types";
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "@/components/issues";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueTypeLogo } from "@/plane-web/components/issue-types";
|
||||
// plane web hooks
|
||||
import { useFlag, useIssueTypes } from "@/plane-web/hooks/store";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
searchQuery: string;
|
||||
};
|
||||
|
||||
export const FilterIssueTypes: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
// states
|
||||
const [itemsToRender, setItemsToRender] = useState(5);
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
// hooks
|
||||
const { workspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const { data: workspaceIssueTypes, getProjectIssueTypes } = useIssueTypes();
|
||||
const { getProjectById } = useProject();
|
||||
const isIssueTypeDisplayEnabled = useFlag(workspaceSlug?.toString(), "ISSUE_TYPE_DISPLAY");
|
||||
// derived values
|
||||
const projectId = routerProjectId?.toString();
|
||||
const projectDetails = getProjectById(projectId);
|
||||
const issueTypes = projectId ? getProjectIssueTypes(projectId, false) : workspaceIssueTypes;
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
|
||||
// Return null if issue type is not enabled for the project
|
||||
if (!isIssueTypeDisplayEnabled || (projectId && !projectDetails?.is_issue_type_enabled)) return null;
|
||||
|
||||
const sortedOptions = useMemo(() => {
|
||||
const filteredOptions = (Object.values(issueTypes) || []).filter((issueType) =>
|
||||
issueType.name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return sortBy(filteredOptions, [
|
||||
(issueType) => issueType.id && !appliedFilters?.includes(issueType.id),
|
||||
(issueType) => issueType.name?.toLowerCase(),
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchQuery]);
|
||||
|
||||
const handleViewToggle = () => {
|
||||
if (!sortedOptions) return;
|
||||
|
||||
if (itemsToRender === sortedOptions.length) setItemsToRender(5);
|
||||
else setItemsToRender(sortedOptions.length);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<FilterHeader
|
||||
title={`Issue Type ${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||
isPreviewEnabled={previewEnabled}
|
||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{sortedOptions ? (
|
||||
sortedOptions.length > 0 ? (
|
||||
<>
|
||||
{sortedOptions.slice(0, itemsToRender).map((issueType) => (
|
||||
<FilterOption
|
||||
key={issueType.id}
|
||||
isChecked={issueType.id && appliedFilters?.includes(issueType.id) ? true : false}
|
||||
onClick={() => issueType.id && handleUpdate(issueType.id)}
|
||||
icon={
|
||||
<IssueTypeLogo
|
||||
icon_props={issueType?.logo_props?.icon}
|
||||
size={10}
|
||||
containerSize={16}
|
||||
isDefault={issueType?.is_default}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
projectId
|
||||
? issueType.name
|
||||
: `${issueType.name}: ${getProjectById(issueType.project_ids?.[0])?.name}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{sortedOptions.length > 5 && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-8 text-xs font-medium text-custom-primary-100"
|
||||
onClick={handleViewToggle}
|
||||
>
|
||||
{itemsToRender === sortedOptions.length ? "View less" : "View all"}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-2">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
// plane web hooks
|
||||
const { getIssueTypeProperties, getProjectActiveIssueTypes, getProjectDefaultIssueType } = useIssueTypes();
|
||||
const { getIssueTypeProperties, getProjectIssueTypes, getProjectDefaultIssueType } = useIssueTypes();
|
||||
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
@@ -180,7 +180,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
|
||||
if (!projectId) return;
|
||||
|
||||
const projectIssueTypes = getProjectActiveIssueTypes(projectId);
|
||||
const projectIssueTypes = getProjectIssueTypes(projectId, true);
|
||||
const defaultIssueType = getProjectDefaultIssueType(projectId);
|
||||
|
||||
// if data is not present, set active type id to the default type id of the project
|
||||
|
||||
@@ -57,7 +57,7 @@ export class IssueTypesService extends APIService {
|
||||
|
||||
async enableIssueTypes(workspaceSlug: string, projectId: string): Promise<string> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/default-issue-types/`)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => response?.data?.issue_type_id)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
||||
@@ -29,9 +29,9 @@ export interface IIssueProperty<T extends EIssuePropertyType> extends TIssueProp
|
||||
getPropertyOptionById: (propertyOptionId: string) => IIssuePropertyOption | undefined;
|
||||
// helper actions
|
||||
updatePropertyData: (propertyData: TIssueProperty<EIssuePropertyType>) => void;
|
||||
addOrUpdatePropertyOptions: (propertyOptionsData: TIssuePropertyOption[]) => void;
|
||||
// actions
|
||||
updateProperty: (issueTypeId: string, propertyData: TIssuePropertyPayload) => Promise<void>;
|
||||
addOrUpdatePropertyOptions: (propertyOptionsData: TIssuePropertyOption[]) => void;
|
||||
createPropertyOption: (propertyOption: Partial<TIssuePropertyOption>) => Promise<TIssuePropertyOption | undefined>;
|
||||
deletePropertyOption: (propertyOptionId: string) => Promise<void>;
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export class IssueProperty<T extends EIssuePropertyType> implements IIssueProper
|
||||
try {
|
||||
// add or update property option
|
||||
for (const option of propertyOptionsData) {
|
||||
if (!option.id) return;
|
||||
if (!option.id) continue;
|
||||
const existingPropertyOption = this.getPropertyOptionById(option.id);
|
||||
if (existingPropertyOption) {
|
||||
// update the existing property option
|
||||
|
||||
@@ -29,7 +29,10 @@ export interface IIssueType extends TIssueType {
|
||||
getPropertyById: <T extends EIssuePropertyType>(propertyId: string) => IIssueProperty<T> | undefined;
|
||||
// actions
|
||||
updateType: (issueTypeData: Partial<TIssueType>) => Promise<TIssueType | undefined>;
|
||||
addOrUpdateProperty: (propertyData: TIssueProperty<EIssuePropertyType>, propertyOptions: TIssuePropertyOption[]) => void;
|
||||
addOrUpdateProperty: (
|
||||
propertyData: TIssueProperty<EIssuePropertyType>,
|
||||
propertyOptions: TIssuePropertyOption[]
|
||||
) => void;
|
||||
createProperty: (propertyData: TIssuePropertyPayload) => Promise<TIssueProperty<EIssuePropertyType> | undefined>;
|
||||
deleteProperty: (propertyId: string) => Promise<void>;
|
||||
}
|
||||
@@ -40,12 +43,11 @@ export class IssueType implements IIssueType {
|
||||
name: string | undefined = undefined;
|
||||
description: string | undefined = undefined;
|
||||
logo_props: TLogoProps | undefined = undefined;
|
||||
sort_order: number | undefined = undefined;
|
||||
is_active: boolean | undefined = undefined;
|
||||
is_default: boolean | undefined = undefined;
|
||||
issue_exists: boolean | undefined = undefined;
|
||||
weight: number | undefined = undefined;
|
||||
project: string | undefined = undefined;
|
||||
level: number | undefined = undefined;
|
||||
project_ids: string[] | undefined = undefined;
|
||||
workspace: string | undefined = undefined;
|
||||
created_at: Date | undefined = undefined;
|
||||
created_by: string | undefined = undefined;
|
||||
@@ -66,11 +68,12 @@ export class IssueType implements IIssueType {
|
||||
name: observable.ref,
|
||||
description: observable.ref,
|
||||
logo_props: observable,
|
||||
sort_order: observable.ref,
|
||||
is_active: observable.ref,
|
||||
is_default: observable.ref,
|
||||
issue_exists: observable.ref,
|
||||
weight: observable.ref,
|
||||
level: observable.ref,
|
||||
project_ids: observable.ref,
|
||||
workspace: observable.ref,
|
||||
created_at: observable.ref,
|
||||
created_by: observable.ref,
|
||||
updated_at: observable.ref,
|
||||
@@ -90,12 +93,11 @@ export class IssueType implements IIssueType {
|
||||
this.name = issueTypeData.name;
|
||||
this.description = issueTypeData.description;
|
||||
this.logo_props = issueTypeData.logo_props;
|
||||
this.sort_order = issueTypeData.sort_order;
|
||||
this.is_active = issueTypeData.is_active;
|
||||
this.is_default = issueTypeData.is_default;
|
||||
this.issue_exists = issueTypeData.issue_exists;
|
||||
this.weight = issueTypeData.weight;
|
||||
this.project = issueTypeData.project;
|
||||
this.level = issueTypeData.level;
|
||||
this.project_ids = issueTypeData.project_ids;
|
||||
this.workspace = issueTypeData.workspace;
|
||||
this.created_at = issueTypeData.created_at;
|
||||
this.created_by = issueTypeData.created_by;
|
||||
@@ -117,12 +119,11 @@ export class IssueType implements IIssueType {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
logo_props: this.logo_props,
|
||||
sort_order: this.sort_order,
|
||||
is_active: this.is_active,
|
||||
is_default: this.is_default,
|
||||
issue_exists: this.issue_exists,
|
||||
weight: this.weight,
|
||||
project: this.project,
|
||||
level: this.level,
|
||||
project_ids: this.project_ids,
|
||||
workspace: this.workspace,
|
||||
created_at: this.created_at,
|
||||
created_by: this.created_by,
|
||||
|
||||
@@ -27,9 +27,10 @@ export interface IIssueTypesStore {
|
||||
propertiesFetchedMap: Record<string, boolean>; // project id -> boolean
|
||||
data: Record<string, IIssueType>; // issue type id -> issue type
|
||||
// computed functions
|
||||
getIssueTypeById: (issueTypeId: string) => IIssueType | undefined;
|
||||
getProjectIssuePropertiesLoader: (projectId: string) => TLoader;
|
||||
getProjectIssueTypeIds: (projectId: string) => string[];
|
||||
getProjectActiveIssueTypes: (projectId: string) => Record<string, IIssueType>; // issue type id -> issue type
|
||||
getProjectIssueTypes: (projectId: string, activeOnly: boolean) => Record<string, IIssueType>; // issue type id -> issue type
|
||||
getProjectDefaultIssueType: (projectId: string) => IIssueType | undefined;
|
||||
getIssueTypeProperties: (issueTypeId: string) => TIssueProperty<EIssuePropertyType>[];
|
||||
// helper actions
|
||||
@@ -76,6 +77,13 @@ export class IssueTypes implements IIssueTypesStore {
|
||||
}
|
||||
|
||||
// computed functions
|
||||
/**
|
||||
* @description Get issue type by issue type id
|
||||
* @param issueTypeId
|
||||
* @returns {IIssueType | undefined}
|
||||
*/
|
||||
getIssueTypeById = computedFn((issueTypeId: string) => this.data[issueTypeId]);
|
||||
|
||||
/**
|
||||
* @description Get project issue type loader
|
||||
* @param projectId
|
||||
@@ -91,8 +99,8 @@ export class IssueTypes implements IIssueTypesStore {
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getProjectIssueTypeIds = computedFn((projectId: string) => {
|
||||
const projectIssueTypeIds = Object.keys(this.data).filter(
|
||||
(issueTypeId) => this.data[issueTypeId]?.project === projectId
|
||||
const projectIssueTypeIds = Object.keys(this.data).filter((issueTypeId) =>
|
||||
this.data[issueTypeId]?.project_ids?.includes(projectId)
|
||||
);
|
||||
return projectIssueTypeIds;
|
||||
});
|
||||
@@ -101,10 +109,11 @@ export class IssueTypes implements IIssueTypesStore {
|
||||
* @description Get current project issue types
|
||||
* @returns {Record<string, IIssueType>}
|
||||
*/
|
||||
getProjectActiveIssueTypes = computedFn((projectId: string) => {
|
||||
getProjectIssueTypes = computedFn((projectId: string, activeOnly: boolean) => {
|
||||
const projectIssueTypes = Object.entries(this.data).reduce(
|
||||
(acc, [issueTypeId, issueType]) => {
|
||||
if (issueType.project === projectId && issueType.is_active) {
|
||||
if (issueType.project_ids?.includes(projectId)) {
|
||||
if (activeOnly && !issueType.is_active) return acc;
|
||||
acc[issueTypeId] = issueType;
|
||||
}
|
||||
return acc;
|
||||
@@ -120,7 +129,7 @@ export class IssueTypes implements IIssueTypesStore {
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
getProjectDefaultIssueType = computedFn((projectId: string) => {
|
||||
const projectIssueTypes = this.getProjectActiveIssueTypes(projectId);
|
||||
const projectIssueTypes = this.getProjectIssueTypes(projectId, true);
|
||||
|
||||
const defaultIssueType = Object.values(projectIssueTypes).find((issueType) => issueType.is_default);
|
||||
return defaultIssueType ?? undefined;
|
||||
|
||||
7
web/ee/types/issue-types/issue-types.d.ts
vendored
7
web/ee/types/issue-types/issue-types.d.ts
vendored
@@ -1,18 +1,17 @@
|
||||
// types
|
||||
import { TLogoProps } from "@plane/types";
|
||||
|
||||
// Issue Property Option
|
||||
// Issue Type
|
||||
export type TIssueType = {
|
||||
id: string | undefined;
|
||||
name: string | undefined;
|
||||
description: string | undefined;
|
||||
logo_props: TLogoProps | undefined;
|
||||
sort_order: number | undefined;
|
||||
is_active: boolean | undefined;
|
||||
is_default: boolean | undefined;
|
||||
issue_exists: boolean | undefined;
|
||||
weight: number | undefined;
|
||||
project: string | undefined;
|
||||
level: number | undefined;
|
||||
project_ids: string[] | undefined;
|
||||
workspace: string | undefined;
|
||||
created_at: Date | undefined;
|
||||
created_by: string | undefined;
|
||||
|
||||
Reference in New Issue
Block a user