mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
@@ -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:
|
||||
|
||||
67
apiserver/plane/ee/management/commands/change_ownership.py
Normal file
67
apiserver/plane/ee/management/commands/change_ownership.py
Normal file
@@ -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")
|
||||
)
|
||||
65
apiserver/plane/ee/management/commands/delete_user.py
Normal file
65
apiserver/plane/ee/management/commands/delete_user.py
Normal file
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<TIssueTypeIconPicker> = (props) => {
|
||||
export const IssueTypeIconPicker: React.FC<TIssueTypeIconPicker> = observer((props) => {
|
||||
const {
|
||||
isOpen,
|
||||
handleToggle,
|
||||
@@ -36,6 +38,7 @@ export const IssueTypeIconPicker: React.FC<TIssueTypeIconPicker> = (props) => {
|
||||
disabled = false,
|
||||
dropdownClassName,
|
||||
icon_props,
|
||||
isDefaultIssueType = false,
|
||||
onChange,
|
||||
size,
|
||||
placement = "bottom-start",
|
||||
@@ -71,7 +74,12 @@ export const IssueTypeIconPicker: React.FC<TIssueTypeIconPicker> = (props) => {
|
||||
disabled={disabled}
|
||||
onClick={() => handleToggle(!isOpen)}
|
||||
>
|
||||
<IssueTypeLogo icon_props={icon_props} size={size} containerClassName={iconContainerClassName} />
|
||||
<IssueTypeLogo
|
||||
icon_props={icon_props}
|
||||
isDefault={isDefaultIssueType}
|
||||
size={size}
|
||||
containerClassName={iconContainerClassName}
|
||||
/>
|
||||
</button>
|
||||
</Popover.Button>
|
||||
{isOpen && (
|
||||
@@ -104,4 +112,4 @@ export const IssueTypeIconPicker: React.FC<TIssueTypeIconPicker> = (props) => {
|
||||
</>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ export const IssueTypeLogo: FC<Props> = (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> = (props) => {
|
||||
}}
|
||||
className={cn("flex-shrink-0 grid place-items-center rounded bg-custom-background-80", containerClassName)} // fallback background color
|
||||
>
|
||||
{isDefault ? (
|
||||
{renderDefaultIcon ? (
|
||||
<LayersIcon
|
||||
width={iconSizeMap[size]}
|
||||
height={iconSizeMap[size]}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
// helpers
|
||||
@@ -18,7 +19,7 @@ type Props = {
|
||||
handleFormSubmit: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const CreateOrUpdateIssueTypeForm: React.FC<Props> = (props) => {
|
||||
export const CreateOrUpdateIssueTypeForm: React.FC<Props> = 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> = (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> = (props) => {
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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<TIssueType> = {
|
||||
description: "",
|
||||
};
|
||||
|
||||
export const CreateOrUpdateIssueTypeModal: FC<Props> = (props) => {
|
||||
export const CreateOrUpdateIssueTypeModal: FC<Props> = observer((props) => {
|
||||
const { issueTypeId, isModalOpen, handleModalClose } = props;
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
@@ -131,4 +132,4 @@ export const CreateOrUpdateIssueTypeModal: FC<Props> = (props) => {
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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<TIssueTypeEmptyState> = observer((props) =>
|
||||
const { toggleProPlanModal } = useWorkspaceSubscription();
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [enableIssueTypeConfirmation, setEnableIssueTypeConfirmation] = useState<boolean>(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<TIssueTypeEmptyState> = observer((props) =>
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center min-h-full overflow-y-auto py-10 px-5">
|
||||
<div className={cn("flex flex-col gap-5 md:min-w-[24rem] max-w-[45rem]")}>
|
||||
<div className="flex flex-col gap-1.5 flex-shrink">
|
||||
<h3 className="text-xl font-semibold">
|
||||
{isIssueTypeSettingsEnabled ? "Enable Issue Types" : "Upgrade to enable Issue Types."}
|
||||
</h3>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Shape issues to your work with Issue Types. Customize with icons, backgrounds, and properties and configure
|
||||
them for this project.
|
||||
</p>
|
||||
</div>
|
||||
<Image
|
||||
src={resolvedEmptyStatePath}
|
||||
alt="issue type empty state"
|
||||
width={384}
|
||||
height={250}
|
||||
layout="responsive"
|
||||
lazyBoundary="100%"
|
||||
/>
|
||||
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
|
||||
{isIssueTypeSettingsEnabled ? (
|
||||
<ComicBoxButton
|
||||
label={isLoading ? "Setting up" : "Enable"}
|
||||
title="Once enabled, Issue Types can't be disabled."
|
||||
description="Plane's Issues will become the default issue type for this project and will show up with its icon and background in this project."
|
||||
disabled={isLoading}
|
||||
onClick={() => handleEnableIssueTypes()}
|
||||
/>
|
||||
) : (
|
||||
<Button disabled={isLoading} onClick={() => toggleProPlanModal(true)}>
|
||||
Upgrade
|
||||
</Button>
|
||||
)}
|
||||
<>
|
||||
<AlertModalCore
|
||||
variant="primary"
|
||||
isOpen={enableIssueTypeConfirmation}
|
||||
handleClose={() => 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.{" "}
|
||||
<a
|
||||
href="https://docs.plane.so/core-concepts/issues/issue-types"
|
||||
target="_blank"
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
Read the docs
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
}
|
||||
primaryButtonText={{
|
||||
loading: "Setting up",
|
||||
default: "Enable",
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-center min-h-full overflow-y-auto py-10 px-5">
|
||||
<div className={cn("flex flex-col gap-5 md:min-w-[24rem] max-w-[45rem]")}>
|
||||
<div className="flex flex-col gap-1.5 flex-shrink">
|
||||
<h3 className="text-xl font-semibold">
|
||||
{isIssueTypeSettingsEnabled ? "Enable Issue Types" : "Upgrade to enable Issue Types."}
|
||||
</h3>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Shape issues to your work with Issue Types. Customize with icons, backgrounds, and properties and
|
||||
configure them for this project.
|
||||
</p>
|
||||
</div>
|
||||
<Image
|
||||
src={resolvedEmptyStatePath}
|
||||
alt="issue type empty state"
|
||||
width={384}
|
||||
height={250}
|
||||
layout="responsive"
|
||||
lazyBoundary="100%"
|
||||
/>
|
||||
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
|
||||
{isIssueTypeSettingsEnabled ? (
|
||||
<Button disabled={isLoading} onClick={() => setEnableIssueTypeConfirmation(true)}>
|
||||
Enable
|
||||
</Button>
|
||||
) : (
|
||||
<Button disabled={isLoading} onClick={() => toggleProPlanModal(true)}>
|
||||
Upgrade
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -74,24 +74,17 @@ export const IssueTypeListItem = observer((props: TIssueTypeListItem) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex">
|
||||
{issueTypeDetail?.is_default && (
|
||||
<div
|
||||
className={cn(
|
||||
"py-0.5 px-2 text-xs rounded text-custom-primary-100 bg-transparent border border-custom-primary-100 cursor-default font-medium"
|
||||
)}
|
||||
>
|
||||
Default
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{!issueTypeDetail?.is_default && (
|
||||
<IssueTypeQuickActions
|
||||
issueTypeId={issueTypeId}
|
||||
onEditIssueTypeIdChange={onEditIssueTypeIdChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<IssueTypeQuickActions issueTypeId={issueTypeId} onEditIssueTypeIdChange={onEditIssueTypeIdChange} />
|
||||
</div>
|
||||
{issueTypeDetail?.is_default && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 py-0.5 px-2 text-xs rounded text-custom-primary-100 bg-transparent border border-custom-primary-100 cursor-default font-medium"
|
||||
)}
|
||||
>
|
||||
Default
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
className={cn("p-2")}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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<T extends EIssuePropertyType> = {
|
||||
onChange: (value: TIssuePropertySettingsMap[T]) => void;
|
||||
};
|
||||
|
||||
export const PropertyRadioInputSelect = <T extends EIssuePropertyType>(
|
||||
export const PropertyRadioInputSelect = observer(<T extends EIssuePropertyType>(
|
||||
props: TPropertySettingsConfigurationProps<T>
|
||||
) => {
|
||||
const { settings, settingsConfigurations, isDisabled, onChange } = props;
|
||||
@@ -44,9 +45,9 @@ export const PropertyRadioInputSelect = <T extends EIssuePropertyType>(
|
||||
vertical
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const PropertySettingsConfiguration = <T extends EIssuePropertyType>(
|
||||
export const PropertySettingsConfiguration = observer(<T extends EIssuePropertyType>(
|
||||
props: TPropertySettingsConfigurationProps<T>
|
||||
) => {
|
||||
const { settings, settingsConfigurations, isDisabled, onChange } = props;
|
||||
@@ -64,4 +65,4 @@ export const PropertySettingsConfiguration = <T extends EIssuePropertyType>(
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
) : (
|
||||
<span className="px-2">{getIssuePropertyTypeDisplayName(propertyType, propertyRelationType)}</span>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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<boolean>(false);
|
||||
@@ -69,4 +70,4 @@ export const PropertyMandatoryFieldToggle = (props: TPropertyMandatoryFieldToggl
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -74,15 +74,17 @@ export const IssueTypeQuickActions: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="shadow"
|
||||
tooltipContent={!!isIssueTypeEnabled ? "Click to disable" : "Click to enable"}
|
||||
position="bottom"
|
||||
>
|
||||
<div className="w-12">
|
||||
<ToggleSwitch value={!!isIssueTypeEnabled} onChange={handleEnableDisable} disabled={isLoading} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!issueTypeDetail?.is_default && (
|
||||
<Tooltip
|
||||
className="shadow"
|
||||
tooltipContent={!!isIssueTypeEnabled ? "Click to disable" : "Click to enable"}
|
||||
position="bottom"
|
||||
>
|
||||
<div className="w-12">
|
||||
<ToggleSwitch value={!!isIssueTypeEnabled} onChange={handleEnableDisable} disabled={isLoading} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -115,7 +115,7 @@ export const CloudEditionBadge = observer(() => {
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="accent-primary"
|
||||
className="w-full cursor-pointer rounded-2xl px-4 py-1.5 text-center text-sm font-medium outline-none"
|
||||
className="w-fit min-w-24 cursor-pointer rounded-2xl px-4 py-1 text-center text-sm font-medium outline-none"
|
||||
onClick={handleProPlanPurchaseModalOpen}
|
||||
>
|
||||
{renderButtonText()}
|
||||
@@ -126,10 +126,10 @@ export const CloudEditionBadge = observer(() => {
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="accent-primary"
|
||||
className="w-full cursor-pointer rounded-2xl px-3 py-1.5 text-center text-sm font-medium outline-none"
|
||||
className="w-fit cursor-pointer rounded-2xl px-4 py-1 text-center text-sm font-medium outline-none"
|
||||
onClick={handlePaidPlanSuccessModalOpen}
|
||||
>
|
||||
<Image src={PlaneLogo} alt="Plane Pro" width={14} height={14} />
|
||||
<Image src={PlaneLogo} alt="Plane Pro" width={12} height={12} />
|
||||
Plane Pro
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -13,12 +13,8 @@ export const useIssueProperty = <T extends EIssuePropertyType>(
|
||||
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<T>(propertyId);
|
||||
if (!issueProperty) {
|
||||
console.warn("useIssueProperty: issueProperty is not found");
|
||||
}
|
||||
return issueProperty;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user