diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index ecc83630f2..ba1999485a 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1178,8 +1178,8 @@ class CycleAnalyticsEndpoint(BaseAPIView): estimate__type="points", ).exists() - assignee_distribution = {} - label_distribution = {} + assignee_distribution = [] + label_distribution = [] completion_chart = {} if analytic_type == "points" and estimate_type: diff --git a/apiserver/plane/ee/management/commands/change_ownership.py b/apiserver/plane/ee/management/commands/change_ownership.py new file mode 100644 index 0000000000..0e0a289c1c --- /dev/null +++ b/apiserver/plane/ee/management/commands/change_ownership.py @@ -0,0 +1,67 @@ +# Django imports +from django.core.management import BaseCommand, CommandError + + +# Module imports +from plane.db.models import User, Workspace, WorkspaceMember + + +class Command(BaseCommand): + help = "Change the ownership of the workspace" + + def add_arguments(self, parser): + # Positional argument + parser.add_argument( + "--email", type=str, help="user email", required=True + ) + parser.add_argument( + "--workspace_slug", type=str, help="workspace slug", required=True + ) + + def handle(self, *args, **options): + # get the user email from console + email = options.get("email", False) + workspace_slug = options.get("workspace_slug", False) + + # raise error if email is not present + if not email or not workspace_slug: + raise CommandError("Error: Email and workspace slug is required") + + # Fetch the user + user = User.objects.filter(email=email).first() + + # Raise error if the user is not present + if not user: + raise CommandError(f"Error: User with {email} does not exists") + + # Fetch the workspace + workspace = Workspace.objects.filter(slug=workspace_slug).first() + + # Raise error if the workspace is not present + if not workspace: + raise CommandError( + f"Error: Workspace with {workspace_slug} does not exists" + ) + + # Change the ownership of the workspace + workspace.owner = user + workspace.save(update_fields=["owner"]) + + # Add the user to the workspace + workspace_member = WorkspaceMember.objects.filter( + workspace=workspace, member=user + ).first() + + # If the user is not present in the workspace, create the user + if not workspace_member: + workspace_member = WorkspaceMember.objects.create( + workspace=workspace, member=user, role=20 + ) + else: + workspace_member.role = 20 + workspace_member.save(update_fields=["role"]) + + # Print the success message + self.stdout.write( + self.style.SUCCESS("Workspace ownership changed successfully") + ) diff --git a/apiserver/plane/ee/management/commands/delete_user.py b/apiserver/plane/ee/management/commands/delete_user.py new file mode 100644 index 0000000000..b708563c98 --- /dev/null +++ b/apiserver/plane/ee/management/commands/delete_user.py @@ -0,0 +1,65 @@ +# Django imports +from django.core.management import BaseCommand, CommandError +from django.db import connection + +# Module imports +from plane.db.models import User + + +class Command(BaseCommand): + help = "Delete the user" + + def add_arguments(self, parser): + # Positional argument + parser.add_argument( + "--email", type=str, help="user email", required=True + ) + + def handle_lingering_constraints(self, user): + """Delete the related objects from the blacklistedtoken and outstandingtoken tables""" + with connection.cursor() as cursor: + # First, delete entries from the blacklistedtoken table + cursor.execute( + """ + DELETE FROM token_blacklist_blacklistedtoken + WHERE token_id IN ( + SELECT id FROM token_blacklist_outstandingtoken + WHERE user_id = %s + ) + """, + [user.id], + ) + + # Then, delete entries from the outstandingtoken table + cursor.execute( + """ + DELETE FROM token_blacklist_outstandingtoken + WHERE user_id = %s + """, + [user.id], + ) + + def handle(self, *args, **options): + # get the user email from console + email = options.get("email", False) + + # raise error if email is not present + if not email: + raise CommandError("Error: Email is required") + + # Fetch the user + user = User.objects.filter(email=email).first() + + # Raise error if the user is not present + if not user: + raise CommandError(f"Error: User with {email} does not exists") + + # Delete the related objects + self.handle_lingering_constraints(user) + + # Delete the user + user.delete() + + # Print the success message + self.stdout.write(self.style.SUCCESS("User deleted successfully")) + return diff --git a/apiserver/plane/ee/views/app/issue_property/type.py b/apiserver/plane/ee/views/app/issue_property/type.py index 26e2ee350f..f8d1288315 100644 --- a/apiserver/plane/ee/views/app/issue_property/type.py +++ b/apiserver/plane/ee/views/app/issue_property/type.py @@ -182,7 +182,7 @@ class IssueTypeEndpoint(BaseAPIView): ) # Default cannot be made in active - if request.data.get("is_default") and not request.data.get( + if issue_type.is_default and not request.data.get( "is_active" ): return Response( diff --git a/web/core/store/issue/cycle/issue.store.ts b/web/core/store/issue/cycle/issue.store.ts index 7f6ac53ec2..6180449d0d 100644 --- a/web/core/store/issue/cycle/issue.store.ts +++ b/web/core/store/issue/cycle/issue.store.ts @@ -142,16 +142,20 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues { }; updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string | undefined) => { - const distributionUpdates = getDistributionPathsPostUpdate( - prevIssueState, - nextIssueState, - this.rootIssueStore.rootStore.state.stateMap, - this.rootIssueStore.rootStore.projectEstimate?.currentActiveEstimate?.estimatePointById - ); + try { + const distributionUpdates = getDistributionPathsPostUpdate( + prevIssueState, + nextIssueState, + this.rootIssueStore.rootStore.state.stateMap, + this.rootIssueStore.rootStore.projectEstimate?.currentActiveEstimate?.estimatePointById + ); - const cycleId = id ?? this.cycleId; + const cycleId = id ?? this.cycleId; - cycleId && this.rootIssueStore.rootStore.cycle.updateCycleDistribution(distributionUpdates, cycleId); + cycleId && this.rootIssueStore.rootStore.cycle.updateCycleDistribution(distributionUpdates, cycleId); + } catch (e) { + console.warn("could not update cycle statistics"); + } }; /** diff --git a/web/core/store/issue/module/issue.store.ts b/web/core/store/issue/module/issue.store.ts index 266c9d6614..9131c8833f 100644 --- a/web/core/store/issue/module/issue.store.ts +++ b/web/core/store/issue/module/issue.store.ts @@ -98,17 +98,21 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { * @param id */ updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string | undefined) => { - // get distribution updates - const distributionUpdates = getDistributionPathsPostUpdate( - prevIssueState, - nextIssueState, - this.rootIssueStore.rootStore.state.stateMap, - this.rootIssueStore.rootStore.projectEstimate?.currentActiveEstimate?.estimatePointById - ); + try { + // get distribution updates + const distributionUpdates = getDistributionPathsPostUpdate( + prevIssueState, + nextIssueState, + this.rootIssueStore.rootStore.state.stateMap, + this.rootIssueStore.rootStore.projectEstimate?.currentActiveEstimate?.estimatePointById + ); - const moduleId = id ?? this.moduleId; + const moduleId = id ?? this.moduleId; - moduleId && this.rootIssueStore.rootStore.module.updateModuleDistribution(distributionUpdates, moduleId); + moduleId && this.rootIssueStore.rootStore.module.updateModuleDistribution(distributionUpdates, moduleId); + } catch (e) { + console.warn("could not update module statistics"); + } }; /** diff --git a/web/ee/components/issue-types/common/icon-picker.tsx b/web/ee/components/issue-types/common/icon-picker.tsx index 0443a8f255..800e4fdc3b 100644 --- a/web/ee/components/issue-types/common/icon-picker.tsx +++ b/web/ee/components/issue-types/common/icon-picker.tsx @@ -1,5 +1,6 @@ import React, { useRef, useState } from "react"; import { Placement } from "@popperjs/core"; +import { observer } from "mobx-react"; import { usePopper } from "react-popper"; import { Popover } from "@headlessui/react"; // types @@ -21,12 +22,13 @@ export type TIssueTypeIconPicker = { disabled?: boolean; dropdownClassName?: string; icon_props: TLogoProps["icon"]; + isDefaultIssueType?: boolean; onChange: (value: TLogoProps["icon"]) => void; placement?: Placement; size?: TIssueTypeLogoSize; }; -export const IssueTypeIconPicker: React.FC = (props) => { +export const IssueTypeIconPicker: React.FC = observer((props) => { const { isOpen, handleToggle, @@ -36,6 +38,7 @@ export const IssueTypeIconPicker: React.FC = (props) => { disabled = false, dropdownClassName, icon_props, + isDefaultIssueType = false, onChange, size, placement = "bottom-start", @@ -71,7 +74,12 @@ export const IssueTypeIconPicker: React.FC = (props) => { disabled={disabled} onClick={() => handleToggle(!isOpen)} > - + {isOpen && ( @@ -104,4 +112,4 @@ export const IssueTypeIconPicker: React.FC = (props) => { ); -}; +}); diff --git a/web/ee/components/issue-types/common/issue-type-logo.tsx b/web/ee/components/issue-types/common/issue-type-logo.tsx index f83515a7a5..52bd11791a 100644 --- a/web/ee/components/issue-types/common/issue-type-logo.tsx +++ b/web/ee/components/issue-types/common/issue-type-logo.tsx @@ -33,6 +33,7 @@ export const IssueTypeLogo: FC = (props) => { const { icon_props, size = "sm", containerClassName, isDefault = false } = props; // derived values const LucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === icon_props?.name); + const renderDefaultIcon = isDefault && (!icon_props?.name || !icon_props?.background_color); // if no value, return empty fragment if (!icon_props?.name && !isDefault) return <>; @@ -47,7 +48,7 @@ export const IssueTypeLogo: FC = (props) => { }} className={cn("flex-shrink-0 grid place-items-center rounded bg-custom-background-80", containerClassName)} // fallback background color > - {isDefault ? ( + {renderDefaultIcon ? ( Promise; }; -export const CreateOrUpdateIssueTypeForm: React.FC = (props) => { +export const CreateOrUpdateIssueTypeForm: React.FC = observer((props) => { const { formData, isSubmitting, handleFormDataChange, handleModalClose, handleFormSubmit } = props; // state const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); @@ -54,7 +55,8 @@ export const CreateOrUpdateIssueTypeForm: React.FC = (props) => { isOpen={isEmojiPickerOpen} handleToggle={(val: boolean) => setIsEmojiPickerOpen(val)} icon_props={formData?.logo_props?.icon} - className="flex items-center justify-center flex-shrink0" + isDefaultIssueType={!!formData?.is_default} + className="flex items-center justify-center flex-shrink-0" iconContainerClassName="flex items-center justify-center" onChange={(value) => { handleFormDataChange("logo_props", { @@ -103,4 +105,4 @@ export const CreateOrUpdateIssueTypeForm: React.FC = (props) => { ); -}; +}); diff --git a/web/ee/components/issue-types/create-update/modal.tsx b/web/ee/components/issue-types/create-update/modal.tsx index 1a8cb4bf29..bacdde2869 100644 --- a/web/ee/components/issue-types/create-update/modal.tsx +++ b/web/ee/components/issue-types/create-update/modal.tsx @@ -1,4 +1,5 @@ import { FC, useEffect, useState } from "react"; +import { observer } from "mobx-react"; // ui import { EModalPosition, EModalWidth, ModalCore, setToast, TOAST_TYPE } from "@plane/ui"; // helpers @@ -24,7 +25,7 @@ const defaultIssueTypeData: Partial = { description: "", }; -export const CreateOrUpdateIssueTypeModal: FC = (props) => { +export const CreateOrUpdateIssueTypeModal: FC = observer((props) => { const { issueTypeId, isModalOpen, handleModalClose } = props; // states const [isSubmitting, setIsSubmitting] = useState(false); @@ -131,4 +132,4 @@ export const CreateOrUpdateIssueTypeModal: FC = (props) => { /> ); -}; +}); diff --git a/web/ee/components/issue-types/empty-state.tsx b/web/ee/components/issue-types/empty-state.tsx index 5423e83109..3a73ad25ed 100644 --- a/web/ee/components/issue-types/empty-state.tsx +++ b/web/ee/components/issue-types/empty-state.tsx @@ -3,9 +3,7 @@ import { observer } from "mobx-react"; import Image from "next/image"; import { useTheme } from "next-themes"; // ui -import { Button, setToast, TOAST_TYPE } from "@plane/ui"; -// components -import { ComicBoxButton } from "@/components/empty-state"; +import { AlertModalCore, Button, setToast, TOAST_TYPE } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; // plane web hooks @@ -26,6 +24,7 @@ export const IssueTypeEmptyState: FC = observer((props) => const { toggleProPlanModal } = useWorkspaceSubscription(); // states const [isLoading, setIsLoading] = useState(false); + const [enableIssueTypeConfirmation, setEnableIssueTypeConfirmation] = useState(false); // derived values const isIssueTypeSettingsEnabled = useFlag(workspaceSlug, "ISSUE_TYPE_SETTINGS"); const resolvedEmptyStatePath = `/empty-state/issue-types/issue-type-${resolvedTheme === "light" ? "light" : "dark"}.svg`; @@ -53,41 +52,65 @@ export const IssueTypeEmptyState: FC = observer((props) => }; return ( -
-
-
-

- {isIssueTypeSettingsEnabled ? "Enable Issue Types" : "Upgrade to enable Issue Types."} -

-

- Shape issues to your work with Issue Types. Customize with icons, backgrounds, and properties and configure - them for this project. -

-
- issue type empty state -
- {isIssueTypeSettingsEnabled ? ( - handleEnableIssueTypes()} - /> - ) : ( - - )} + <> + setEnableIssueTypeConfirmation(false)} + handleSubmit={() => handleEnableIssueTypes()} + isSubmitting={isLoading} + title="Once enabled, Issue Types can't be disabled." + content={ + <> + Plane's Issues will become the default issue type for this project and will show up with its icon and + background in this project.{" "} + + Read the docs + + . + + } + primaryButtonText={{ + loading: "Setting up", + default: "Enable", + }} + /> +
+
+
+

+ {isIssueTypeSettingsEnabled ? "Enable Issue Types" : "Upgrade to enable Issue Types."} +

+

+ Shape issues to your work with Issue Types. Customize with icons, backgrounds, and properties and + configure them for this project. +

+
+ issue type empty state +
+ {isIssueTypeSettingsEnabled ? ( + + ) : ( + + )} +
-
+ ); }); diff --git a/web/ee/components/issue-types/issue-type-list-item.tsx b/web/ee/components/issue-types/issue-type-list-item.tsx index 54a091caf0..5a0f01a17a 100644 --- a/web/ee/components/issue-types/issue-type-list-item.tsx +++ b/web/ee/components/issue-types/issue-type-list-item.tsx @@ -74,24 +74,17 @@ export const IssueTypeListItem = observer((props: TIssueTypeListItem) => {
- {issueTypeDetail?.is_default && ( -
- Default -
- )} -
- {!issueTypeDetail?.is_default && ( - - )} -
+
+ {issueTypeDetail?.is_default && ( +
+ Default +
+ )} } className={cn("p-2")} diff --git a/web/ee/components/issue-types/properties/attributes/common/property-multi-select.tsx b/web/ee/components/issue-types/properties/attributes/common/property-multi-select.tsx index 598382e558..8f1f8fc9f4 100644 --- a/web/ee/components/issue-types/properties/attributes/common/property-multi-select.tsx +++ b/web/ee/components/issue-types/properties/attributes/common/property-multi-select.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react"; // plane web components import { RadioInput } from "@/components/estimates"; // plane web constants @@ -12,7 +13,7 @@ type TPropertyMultiSelectProps = { onChange: (value: boolean) => void; }; -export const PropertyMultiSelect = (props: TPropertyMultiSelectProps) => { +export const PropertyMultiSelect = observer((props: TPropertyMultiSelectProps) => { const { value, variant = "RELATION_ISSUE", isDisabled = false, onChange } = props; // derived values const MULTI_SELECT_ATTRIBUTES = DROPDOWN_ATTRIBUTES[variant] ?? []; @@ -42,4 +43,4 @@ export const PropertyMultiSelect = (props: TPropertyMultiSelectProps) => { vertical /> ); -}; +}); diff --git a/web/ee/components/issue-types/properties/attributes/common/property-settings-configuration.tsx b/web/ee/components/issue-types/properties/attributes/common/property-settings-configuration.tsx index b9477bd4c9..cc60c3b2c0 100644 --- a/web/ee/components/issue-types/properties/attributes/common/property-settings-configuration.tsx +++ b/web/ee/components/issue-types/properties/attributes/common/property-settings-configuration.tsx @@ -1,4 +1,5 @@ import { get, set } from "lodash"; +import { observer } from "mobx-react"; // components import { RadioInput } from "@/components/estimates"; // plane web constants @@ -11,7 +12,7 @@ type TPropertySettingsConfigurationProps = { onChange: (value: TIssuePropertySettingsMap[T]) => void; }; -export const PropertyRadioInputSelect = ( +export const PropertyRadioInputSelect = observer(( props: TPropertySettingsConfigurationProps ) => { const { settings, settingsConfigurations, isDisabled, onChange } = props; @@ -44,9 +45,9 @@ export const PropertyRadioInputSelect = ( vertical /> ); -}; +}); -export const PropertySettingsConfiguration = ( +export const PropertySettingsConfiguration = observer(( props: TPropertySettingsConfigurationProps ) => { const { settings, settingsConfigurations, isDisabled, onChange } = props; @@ -64,4 +65,4 @@ export const PropertySettingsConfiguration = ( default: return null; } -}; +}); diff --git a/web/ee/components/issue-types/properties/dropdowns/property-type.tsx b/web/ee/components/issue-types/properties/dropdowns/property-type.tsx index 3f7b416282..d216510101 100644 --- a/web/ee/components/issue-types/properties/dropdowns/property-type.tsx +++ b/web/ee/components/issue-types/properties/dropdowns/property-type.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react"; // ui import { CustomSearchSelect } from "@plane/ui"; // helpers @@ -25,7 +26,7 @@ type TPropertyTypeDropdownProps = { error?: string; }; -export const PropertyTypeDropdown = (props: TPropertyTypeDropdownProps) => { +export const PropertyTypeDropdown = observer((props: TPropertyTypeDropdownProps) => { const { issueTypeId, propertyType, @@ -76,4 +77,4 @@ export const PropertyTypeDropdown = (props: TPropertyTypeDropdownProps) => { ) : ( {getIssuePropertyTypeDisplayName(propertyType, propertyRelationType)} ); -}; +}); diff --git a/web/ee/components/issue-types/properties/mandatory-field-toggle.tsx b/web/ee/components/issue-types/properties/mandatory-field-toggle.tsx index 6dc35537e3..8a3e14260e 100644 --- a/web/ee/components/issue-types/properties/mandatory-field-toggle.tsx +++ b/web/ee/components/issue-types/properties/mandatory-field-toggle.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { observer } from "mobx-react"; // ui import { AlertModalCore, Checkbox, Tooltip } from "@plane/ui"; @@ -9,7 +10,7 @@ type TPropertyMandatoryFieldToggleProps = { onMandatoryFieldChange: (value: boolean) => void; }; -export const PropertyMandatoryFieldToggle = (props: TPropertyMandatoryFieldToggleProps) => { +export const PropertyMandatoryFieldToggle = observer((props: TPropertyMandatoryFieldToggleProps) => { const { value, defaultValue, isDisabled = false, onMandatoryFieldChange } = props; // states const [isDefaultResetConfirmationOpen, setIsDefaultResetConfirmationOpen] = useState(false); @@ -69,4 +70,4 @@ export const PropertyMandatoryFieldToggle = (props: TPropertyMandatoryFieldToggl ); -}; +}); diff --git a/web/ee/components/issue-types/quick-actions.tsx b/web/ee/components/issue-types/quick-actions.tsx index 4eb9a83916..a9eb2b5da5 100644 --- a/web/ee/components/issue-types/quick-actions.tsx +++ b/web/ee/components/issue-types/quick-actions.tsx @@ -74,15 +74,17 @@ export const IssueTypeQuickActions: React.FC = observer((props) => { - -
- -
-
+ {!issueTypeDetail?.is_default && ( + +
+ +
+
+ )} ); diff --git a/web/ee/components/license/cloud-badge.tsx b/web/ee/components/license/cloud-badge.tsx index 21f87f6b6b..ce93f5b619 100644 --- a/web/ee/components/license/cloud-badge.tsx +++ b/web/ee/components/license/cloud-badge.tsx @@ -115,7 +115,7 @@ export const CloudEditionBadge = observer(() => { )} diff --git a/web/ee/hooks/store/issue-types/use-issue-property.ts b/web/ee/hooks/store/issue-types/use-issue-property.ts index f4cda82f36..79baaabc39 100644 --- a/web/ee/hooks/store/issue-types/use-issue-property.ts +++ b/web/ee/hooks/store/issue-types/use-issue-property.ts @@ -13,12 +13,8 @@ export const useIssueProperty = ( const context = useContext(StoreContext); if (context === undefined) throw new Error("useIssueProperty must be used within StoreProvider"); if (!propertyId) { - console.warn("useIssueProperty: propertyId is not provided"); return undefined; } const issueProperty = context.issueTypes.data?.[typeId]?.getPropertyById(propertyId); - if (!issueProperty) { - console.warn("useIssueProperty: issueProperty is not found"); - } return issueProperty; }; diff --git a/web/ee/hooks/store/issue-types/use-issue-type.ts b/web/ee/hooks/store/issue-types/use-issue-type.ts index fe5e76e907..241a9f5024 100644 --- a/web/ee/hooks/store/issue-types/use-issue-type.ts +++ b/web/ee/hooks/store/issue-types/use-issue-type.ts @@ -8,12 +8,8 @@ export const useIssueType = (typeId: string | null | undefined): IIssueType | un const context = useContext(StoreContext); if (context === undefined) throw new Error("useIssueType must be used within StoreProvider"); if (!typeId) { - console.warn("useIssueType: typeId is not provided"); return undefined; } const issueType = context.issueTypes.data?.[typeId]; - if (!issueType) { - console.warn("useIssueType: issueType is not found"); - } return issueType; }; diff --git a/web/helpers/distribution-update.helper.ts b/web/helpers/distribution-update.helper.ts index 5fdd09a8c9..c55543fee7 100644 --- a/web/helpers/distribution-update.helper.ts +++ b/web/helpers/distribution-update.helper.ts @@ -242,21 +242,27 @@ export const updateDistribution = (distributionObject: ICycle | IModule, distrib const { id } = assigneeUpdate; // find and update the assignee issue counts - const issuesAssignee = distributionObject.distribution?.assignees?.find((assignee) => assignee.assignee_id === id); - if (issuesAssignee) { - issuesAssignee.completed_issues += assigneeUpdate.completed_issues ?? 0; - issuesAssignee.pending_issues += assigneeUpdate.pending_issues ?? 0; - issuesAssignee.total_issues += assigneeUpdate.total_issues; + if (Array.isArray(distributionObject.distribution?.assignees)) { + const issuesAssignee = distributionObject.distribution?.assignees?.find( + (assignee) => assignee.assignee_id === id + ); + if (issuesAssignee) { + issuesAssignee.completed_issues += assigneeUpdate.completed_issues ?? 0; + issuesAssignee.pending_issues += assigneeUpdate.pending_issues ?? 0; + issuesAssignee.total_issues += assigneeUpdate.total_issues; + } } // find and update the assignee points - const pointsAssignee = distributionObject.estimate_distribution?.assignees?.find( - (assignee) => assignee.assignee_id === id - ); - if (pointsAssignee) { - pointsAssignee.completed_estimates += assigneeUpdate.completed_estimates ?? 0; - pointsAssignee.pending_estimates += assigneeUpdate.pending_estimates ?? 0; - pointsAssignee.total_estimates += assigneeUpdate.total_estimates; + if (Array.isArray(distributionObject.estimate_distribution?.assignees)) { + const pointsAssignee = distributionObject.estimate_distribution?.assignees?.find( + (assignee) => assignee.assignee_id === id + ); + if (pointsAssignee) { + pointsAssignee.completed_estimates += assigneeUpdate.completed_estimates ?? 0; + pointsAssignee.pending_estimates += assigneeUpdate.pending_estimates ?? 0; + pointsAssignee.total_estimates += assigneeUpdate.total_estimates; + } } } @@ -264,19 +270,23 @@ export const updateDistribution = (distributionObject: ICycle | IModule, distrib const { id } = labelUpdate; // find and update the label issue counts - const issuesLabel = distributionObject.distribution?.labels?.find((label) => label.label_id === id); - if (issuesLabel) { - issuesLabel.completed_issues += labelUpdate.completed_issues ?? 0; - issuesLabel.pending_issues += labelUpdate.pending_issues ?? 0; - issuesLabel.total_issues += labelUpdate.total_issues; + if (Array.isArray(distributionObject.distribution?.labels)) { + const issuesLabel = distributionObject.distribution?.labels?.find((label) => label.label_id === id); + if (issuesLabel) { + issuesLabel.completed_issues += labelUpdate.completed_issues ?? 0; + issuesLabel.pending_issues += labelUpdate.pending_issues ?? 0; + issuesLabel.total_issues += labelUpdate.total_issues; + } } // find and update the label points - const pointsLabel = distributionObject.estimate_distribution?.labels?.find((label) => label.label_id === id); - if (pointsLabel) { - pointsLabel.completed_estimates += labelUpdate.completed_estimates ?? 0; - pointsLabel.pending_estimates += labelUpdate.pending_estimates ?? 0; - pointsLabel.total_estimates += labelUpdate.total_estimates; + if (Array.isArray(distributionObject.estimate_distribution?.labels)) { + const pointsLabel = distributionObject.estimate_distribution?.labels?.find((label) => label.label_id === id); + if (pointsLabel) { + pointsLabel.completed_estimates += labelUpdate.completed_estimates ?? 0; + pointsLabel.pending_estimates += labelUpdate.pending_estimates ?? 0; + pointsLabel.total_estimates += labelUpdate.total_estimates; + } } } };