[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:
Prateek Shourya
2024-08-16 18:27:11 +05:30
committed by GitHub
parent 67616c4500
commit 162fbcfb7e
15 changed files with 465 additions and 82 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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]) => ({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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