diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index cc59593e12..81e68ad57c 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -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 diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 46d5e117c7..3520f62177 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -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 \ No newline at end of file +from .recent_visit import UserRecentVisit diff --git a/apiserver/plane/ee/serializers/app/issue_property.py b/apiserver/plane/ee/serializers/app/issue_property.py index 465a6cf2a5..5e78d02962 100644 --- a/apiserver/plane/ee/serializers/app/issue_property.py +++ b/apiserver/plane/ee/serializers/app/issue_property.py @@ -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", ] diff --git a/apiserver/plane/ee/views/app/issue_property/type.py b/apiserver/plane/ee/views/app/issue_property/type.py index 026f5b34c8..26e2ee350f 100644 --- a/apiserver/plane/ee/views/app/issue_property/type.py +++ b/apiserver/plane/ee/views/app/issue_property/type.py @@ -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) diff --git a/apiserver/plane/ee/views/app/issue_property/value.py b/apiserver/plane/ee/views/app/issue_property/value.py index 28d6310a6d..37c8d1f966 100644 --- a/apiserver/plane/ee/views/app/issue_property/value.py +++ b/apiserver/plane/ee/views/app/issue_property/value.py @@ -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), diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 713276d0cd..95a14b9b02 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -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(): diff --git a/web/ee/components/issue-types/dropdowns/issue-type.tsx b/web/ee/components/issue-types/dropdowns/issue-type.tsx index 9f401bea9b..f5215e1067 100644 --- a/web/ee/components/issue-types/dropdowns/issue-type.tsx +++ b/web/ee/components/issue-types/dropdowns/issue-type.tsx @@ -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]) => ({ diff --git a/web/ee/components/issues/filters/applied-filters/issue-types.tsx b/web/ee/components/issues/filters/applied-filters/issue-types.tsx index dc5e4ee58a..702e73f88e 100644 --- a/web/ee/components/issues/filters/applied-filters/issue-types.tsx +++ b/web/ee/components/issues/filters/applied-filters/issue-types.tsx @@ -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 = 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 ( +
+ + {issueType.name} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/ee/components/issues/filters/issue-types.tsx b/web/ee/components/issues/filters/issue-types.tsx index e08857af57..cd4f0fcf0a 100644 --- a/web/ee/components/issues/filters/issue-types.tsx +++ b/web/ee/components/issues/filters/issue-types.tsx @@ -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 = 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 ( +
+ 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {sortedOptions ? ( + sortedOptions.length > 0 ? ( + <> + {sortedOptions.slice(0, itemsToRender).map((issueType) => ( + issueType.id && handleUpdate(issueType.id)} + icon={ + + } + title={ + projectId + ? issueType.name + : `${issueType.name}: ${getProjectById(issueType.project_ids?.[0])?.name}` + } + /> + ))} + {sortedOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} +
+ ); +}); diff --git a/web/ee/components/issues/issue-modal/form.tsx b/web/ee/components/issues/issue-modal/form.tsx index 32ee07eda2..381d958abb 100644 --- a/web/ee/components/issues/issue-modal/form.tsx +++ b/web/ee/components/issues/issue-modal/form.tsx @@ -90,7 +90,7 @@ export const IssueFormRoot: FC = 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 = 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 diff --git a/web/ee/services/issue-types/issue-types.service.ts b/web/ee/services/issue-types/issue-types.service.ts index ac3e4768e8..c6ab507fa4 100644 --- a/web/ee/services/issue-types/issue-types.service.ts +++ b/web/ee/services/issue-types/issue-types.service.ts @@ -57,7 +57,7 @@ export class IssueTypesService extends APIService { async enableIssueTypes(workspaceSlug: string, projectId: string): Promise { 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; }); diff --git a/web/ee/store/issue-types/issue-property.ts b/web/ee/store/issue-types/issue-property.ts index 1c95646b32..51225df1bb 100644 --- a/web/ee/store/issue-types/issue-property.ts +++ b/web/ee/store/issue-types/issue-property.ts @@ -29,9 +29,9 @@ export interface IIssueProperty extends TIssueProp getPropertyOptionById: (propertyOptionId: string) => IIssuePropertyOption | undefined; // helper actions updatePropertyData: (propertyData: TIssueProperty) => void; + addOrUpdatePropertyOptions: (propertyOptionsData: TIssuePropertyOption[]) => void; // actions updateProperty: (issueTypeId: string, propertyData: TIssuePropertyPayload) => Promise; - addOrUpdatePropertyOptions: (propertyOptionsData: TIssuePropertyOption[]) => void; createPropertyOption: (propertyOption: Partial) => Promise; deletePropertyOption: (propertyOptionId: string) => Promise; } @@ -193,7 +193,7 @@ export class IssueProperty 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 diff --git a/web/ee/store/issue-types/issue-type.ts b/web/ee/store/issue-types/issue-type.ts index a6f727a2ef..ae2adab9bc 100644 --- a/web/ee/store/issue-types/issue-type.ts +++ b/web/ee/store/issue-types/issue-type.ts @@ -29,7 +29,10 @@ export interface IIssueType extends TIssueType { getPropertyById: (propertyId: string) => IIssueProperty | undefined; // actions updateType: (issueTypeData: Partial) => Promise; - addOrUpdateProperty: (propertyData: TIssueProperty, propertyOptions: TIssuePropertyOption[]) => void; + addOrUpdateProperty: ( + propertyData: TIssueProperty, + propertyOptions: TIssuePropertyOption[] + ) => void; createProperty: (propertyData: TIssuePropertyPayload) => Promise | undefined>; deleteProperty: (propertyId: string) => Promise; } @@ -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, diff --git a/web/ee/store/issue-types/issue-types.store.ts b/web/ee/store/issue-types/issue-types.store.ts index 015d44c0fe..7934962654 100644 --- a/web/ee/store/issue-types/issue-types.store.ts +++ b/web/ee/store/issue-types/issue-types.store.ts @@ -27,9 +27,10 @@ export interface IIssueTypesStore { propertiesFetchedMap: Record; // project id -> boolean data: Record; // issue type id -> issue type // computed functions + getIssueTypeById: (issueTypeId: string) => IIssueType | undefined; getProjectIssuePropertiesLoader: (projectId: string) => TLoader; getProjectIssueTypeIds: (projectId: string) => string[]; - getProjectActiveIssueTypes: (projectId: string) => Record; // issue type id -> issue type + getProjectIssueTypes: (projectId: string, activeOnly: boolean) => Record; // issue type id -> issue type getProjectDefaultIssueType: (projectId: string) => IIssueType | undefined; getIssueTypeProperties: (issueTypeId: string) => TIssueProperty[]; // 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} */ - 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; diff --git a/web/ee/types/issue-types/issue-types.d.ts b/web/ee/types/issue-types/issue-types.d.ts index 979f88f637..bfdc17a52f 100644 --- a/web/ee/types/issue-types/issue-types.d.ts +++ b/web/ee/types/issue-types/issue-types.d.ts @@ -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;