mirror of
https://github.com/makeplane/plane.git
synced 2025-12-22 22:59:33 +01:00
[WEB-3751] chore: work item state icon improvement (#6960)
* chore: return order based on group * chore: order for workspace stats endpoint * chore: state response updated * chore: state icon types updated * chore: state icon updated * chore: state settings new icon implementation * chore: icon implementation * chore: code refactor * chore: code refactor * chore: code refactor * fix: order field type --------- Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
This commit is contained in:
committed by
GitHub
parent
baabb82669
commit
f5449c8f93
@@ -172,14 +172,14 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
states = [
|
||||
{
|
||||
"name": "Backlog",
|
||||
"color": "#A3A3A3",
|
||||
"color": "#60646C",
|
||||
"sequence": 15000,
|
||||
"group": "backlog",
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
"name": "Todo",
|
||||
"color": "#3A3A3A",
|
||||
"color": "#60646C",
|
||||
"sequence": 25000,
|
||||
"group": "unstarted",
|
||||
},
|
||||
@@ -191,13 +191,13 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
},
|
||||
{
|
||||
"name": "Done",
|
||||
"color": "#16A34A",
|
||||
"color": "#46A758",
|
||||
"sequence": 45000,
|
||||
"group": "completed",
|
||||
},
|
||||
{
|
||||
"name": "Cancelled",
|
||||
"color": "#EF4444",
|
||||
"color": "#9AA4BC",
|
||||
"sequence": 55000,
|
||||
"group": "cancelled",
|
||||
},
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from plane.db.models import State
|
||||
|
||||
|
||||
class StateSerializer(BaseSerializer):
|
||||
order = serializers.FloatField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = State
|
||||
fields = [
|
||||
@@ -18,6 +20,7 @@ class StateSerializer(BaseSerializer):
|
||||
"default",
|
||||
"description",
|
||||
"sequence",
|
||||
"order",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
@@ -275,14 +275,14 @@ class ProjectViewSet(BaseViewSet):
|
||||
states = [
|
||||
{
|
||||
"name": "Backlog",
|
||||
"color": "#A3A3A3",
|
||||
"color": "#60646C",
|
||||
"sequence": 15000,
|
||||
"group": "backlog",
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
"name": "Todo",
|
||||
"color": "#3A3A3A",
|
||||
"color": "#60646C",
|
||||
"sequence": 25000,
|
||||
"group": "unstarted",
|
||||
},
|
||||
@@ -294,13 +294,13 @@ class ProjectViewSet(BaseViewSet):
|
||||
},
|
||||
{
|
||||
"name": "Done",
|
||||
"color": "#16A34A",
|
||||
"color": "#46A758",
|
||||
"sequence": 45000,
|
||||
"group": "completed",
|
||||
},
|
||||
{
|
||||
"name": "Cancelled",
|
||||
"color": "#EF4444",
|
||||
"color": "#9AA4BC",
|
||||
"sequence": 55000,
|
||||
"group": "cancelled",
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Python imports
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
|
||||
# Django imports
|
||||
from django.db.utils import IntegrityError
|
||||
@@ -74,7 +75,19 @@ class StateViewSet(BaseViewSet):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
states = StateSerializer(self.get_queryset(), many=True).data
|
||||
|
||||
grouped_states = defaultdict(list)
|
||||
for state in states:
|
||||
grouped_states[state["group"]].append(state)
|
||||
|
||||
for group, group_states in grouped_states.items():
|
||||
count = len(group_states)
|
||||
|
||||
for index, state in enumerate(group_states, start=1):
|
||||
state["order"] = index / count
|
||||
|
||||
grouped = request.GET.get("grouped", False)
|
||||
|
||||
if grouped == "true":
|
||||
state_dict = {}
|
||||
for key, value in groupby(
|
||||
@@ -83,6 +96,7 @@ class StateViewSet(BaseViewSet):
|
||||
):
|
||||
state_dict[str(key)] = list(value)
|
||||
return Response(state_dict, status=status.HTTP_200_OK)
|
||||
|
||||
return Response(states, status=status.HTTP_200_OK)
|
||||
|
||||
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
|
||||
|
||||
@@ -8,6 +8,7 @@ from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import State
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
from plane.utils.cache import cache_response
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class WorkspaceStatesEndpoint(BaseAPIView):
|
||||
@@ -22,5 +23,16 @@ class WorkspaceStatesEndpoint(BaseAPIView):
|
||||
project__archived_at__isnull=True,
|
||||
is_triage=False,
|
||||
)
|
||||
|
||||
grouped_states = defaultdict(list)
|
||||
for state in states:
|
||||
grouped_states[state.group].append(state)
|
||||
|
||||
for group, group_states in grouped_states.items():
|
||||
count = len(group_states)
|
||||
|
||||
for index, state in enumerate(group_states, start=1):
|
||||
state.order = index / count
|
||||
|
||||
serializer = StateSerializer(states, many=True).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
7
packages/constants/src/icon.ts
Normal file
7
packages/constants/src/icon.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum EIconSize {
|
||||
XS = "xs",
|
||||
SM = "sm",
|
||||
MD = "md",
|
||||
LG = "lg",
|
||||
XL = "xl",
|
||||
}
|
||||
@@ -32,3 +32,4 @@ export * from "./dashboard";
|
||||
export * from "./page";
|
||||
export * from "./emoji";
|
||||
export * from "./subscription";
|
||||
export * from "./icon";
|
||||
|
||||
1
packages/types/src/state.d.ts
vendored
1
packages/types/src/state.d.ts
vendored
@@ -12,6 +12,7 @@ export interface IState {
|
||||
project_id: string;
|
||||
sequence: number;
|
||||
workspace_id: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface IStateLite {
|
||||
|
||||
@@ -1,39 +1,27 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "../type";
|
||||
import { DashedCircle } from "./dashed-circle";
|
||||
|
||||
export const BacklogGroupIcon: React.FC<ISvgIcons> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#a3a3a3",
|
||||
}) => (
|
||||
color = "#60646C",
|
||||
}) => {
|
||||
// SVG parameters
|
||||
const viewBoxSize = 16;
|
||||
const center = viewBoxSize / 2;
|
||||
const radius = 6;
|
||||
return (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 323.15 323.03"
|
||||
>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path
|
||||
fill={color}
|
||||
d="M163.42,322.92A172.12,172.12,0,0,1,104.8,312.7c-3.92-1.4-5.22-3.05-3.07-7.1,2.4-4.52,3-11.38,6.64-13.48s9.34,2.47,14.23,3.81c29.55,8.11,58.78,7.25,87.57-3.31,4.08-1.5,5.86-1.05,7.09,3.21a82.63,82.63,0,0,0,4.6,11c1.19,2.57,1,4.06-2,5.2a163.84,163.84,0,0,1-40.05,9.76C173.84,322.45,167.89,323.34,163.42,322.92Z"
|
||||
/>
|
||||
<path
|
||||
fill={color}
|
||||
d="M.07,163a174.76,174.76,0,0,1,10.07-58c1.59-4.57,3.53-5.59,7.8-3.2a61,61,0,0,0,10.11,4.19c3.11,1.06,4.07,2.46,2.71,5.79-6.43,15.73-9.17,32.33-9.23,49.14a132.65,132.65,0,0,0,8.17,47.35c2.44,6.5,2.33,6.57-4.06,9.35-3.35,1.45-6.83,2.63-10.11,4.23-2.44,1.19-3.54.49-4.43-1.86a162.3,162.3,0,0,1-10-41C.51,173.12-.24,167.17.07,163Z"
|
||||
/>
|
||||
<path
|
||||
fill={color}
|
||||
d="M323,160.16a169.68,169.68,0,0,1-10.2,58.09c-1.45,4.08-3.21,5.07-7.14,3a105.3,105.3,0,0,0-11.48-4.81c-2.23-.85-3.2-1.85-2.16-4.41a133.86,133.86,0,0,0,9.57-48.59,132,132,0,0,0-8.9-50.69c-1.67-4.24-.8-5.79,3.29-7a84,84,0,0,0,11-4.62c2.65-1.24,4.05-.82,5.16,2.12a159.68,159.68,0,0,1,9.68,39C322.56,148.71,323.52,155.17,323,160.16Z"
|
||||
/>
|
||||
<path
|
||||
fill={color}
|
||||
d="M161.59,0a164.28,164.28,0,0,1,58,10.72c2.81,1,3.75,2,2.41,4.93-2,4.38-3.86,8.84-5.5,13.37-.93,2.56-2.28,2.77-4.53,1.87a137.94,137.94,0,0,0-99.35-.52c-3.43,1.32-5.3,1.35-6.45-2.69a50.33,50.33,0,0,0-4.55-11c-2.25-3.93-.36-5.11,2.9-6.29A165.32,165.32,0,0,1,161.59,0Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<DashedCircle center={center} radius={radius} color={color} percentage={0} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ISvgIcons } from "../type";
|
||||
|
||||
export const CancelledGroupIcon: React.FC<ISvgIcons> = ({
|
||||
className = "",
|
||||
color = "#ef4444",
|
||||
color = "#9AA4BC",
|
||||
height = "20",
|
||||
width = "20",
|
||||
...rest
|
||||
@@ -17,16 +17,11 @@ export const CancelledGroupIcon: React.FC<ISvgIcons> = ({
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...rest}
|
||||
>
|
||||
<g clipPath="url(#clip0_4052_100277)">
|
||||
<path
|
||||
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM11.1018 4.89826C11.3947 5.19115 11.3947 5.66603 11.1018 5.95892L9.06068 8.00002L11.1018 10.0411C11.3947 10.334 11.3947 10.8089 11.1018 11.1018C10.8089 11.3947 10.334 11.3947 10.0411 11.1018L8.00002 9.06068L5.95892 11.1018C5.66603 11.3947 5.19115 11.3947 4.89826 11.1018C4.60537 10.8089 4.60537 10.334 4.89826 10.0411L6.93936 8.00002L4.89826 5.95892C4.60537 5.66603 4.60537 5.19115 4.89826 4.89826C5.19115 4.60537 5.66603 4.60537 5.95892 4.89826L8.00002 6.93936L10.0411 4.89826C10.334 4.60537 10.8089 4.60537 11.1018 4.89826Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4052_100277">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ISvgIcons } from "../type";
|
||||
|
||||
export const CompletedGroupIcon: React.FC<ISvgIcons> = ({
|
||||
className = "",
|
||||
color = "#16a34a",
|
||||
color = "#46A758",
|
||||
height = "20",
|
||||
width = "20",
|
||||
...rest
|
||||
@@ -18,7 +18,9 @@ export const CompletedGroupIcon: React.FC<ISvgIcons> = ({
|
||||
{...rest}
|
||||
>
|
||||
<path
|
||||
d="M6.80486 9.80731L4.84856 7.85103C4.73197 7.73443 4.58542 7.67478 4.4089 7.67208C4.23238 7.66937 4.08312 7.72902 3.96113 7.85103C3.83913 7.97302 3.77814 8.12093 3.77814 8.29474C3.77814 8.46855 3.83913 8.61645 3.96113 8.73844L6.27206 11.0494C6.42428 11.2016 6.60188 11.2777 6.80486 11.2777C7.00782 11.2777 7.18541 11.2016 7.33764 11.0494L12.0227 6.36435C12.1393 6.24776 12.1989 6.10121 12.2016 5.92469C12.2043 5.74817 12.1447 5.59891 12.0227 5.47692C11.9007 5.35493 11.7528 5.29393 11.579 5.29393C11.4051 5.29393 11.2572 5.35493 11.1353 5.47692L6.80486 9.80731ZM8.00141 16C6.89494 16 5.85491 15.79 4.88132 15.3701C3.90772 14.9502 3.06082 14.3803 2.34064 13.6604C1.62044 12.9405 1.05028 12.094 0.63017 11.1208C0.210057 10.1477 0 9.10788 0 8.00141C0 6.89494 0.209966 5.85491 0.629896 4.88132C1.04983 3.90772 1.61972 3.06082 2.33958 2.34064C3.05946 1.62044 3.90598 1.05028 4.87915 0.630171C5.8523 0.210058 6.89212 0 7.99859 0C9.10506 0 10.1451 0.209966 11.1187 0.629897C12.0923 1.04983 12.9392 1.61972 13.6594 2.33959C14.3796 3.05946 14.9497 3.90598 15.3698 4.87915C15.7899 5.8523 16 6.89212 16 7.99859C16 9.10506 15.79 10.1451 15.3701 11.1187C14.9502 12.0923 14.3803 12.9392 13.6604 13.6594C12.9405 14.3796 12.094 14.9497 11.1208 15.3698C10.1477 15.7899 9.10788 16 8.00141 16ZM8 14.7369C9.88071 14.7369 11.4737 14.0842 12.779 12.779C14.0842 11.4737 14.7369 9.88071 14.7369 8C14.7369 6.11929 14.0842 4.52631 12.779 3.22104C11.4737 1.91577 9.88071 1.26314 8 1.26314C6.11929 1.26314 4.52631 1.91577 3.22104 3.22104C1.91577 4.52631 1.26314 6.11929 1.26314 8C1.26314 9.88071 1.91577 11.4737 3.22104 12.779C4.52631 14.0842 6.11929 14.7369 8 14.7369Z"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM11.3587 6.18828C11.6007 5.85214 11.5244 5.38343 11.1882 5.14141C10.8521 4.89938 10.3834 4.97568 10.1414 5.31183L7.03706 9.62335L5.25956 7.97751C4.95563 7.69609 4.4811 7.71434 4.19968 8.01828C3.91826 8.32221 3.93651 8.79673 4.24045 9.07815L6.64045 11.3004C6.79816 11.4464 7.01095 11.5178 7.22481 11.4963C7.43868 11.4749 7.63307 11.3627 7.75865 11.1883L11.3587 6.18828Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
40
packages/ui/src/icons/state/dashed-circle.tsx
Normal file
40
packages/ui/src/icons/state/dashed-circle.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as React from "react";
|
||||
|
||||
interface DashedCircleProps {
|
||||
center: number;
|
||||
radius: number;
|
||||
color: string;
|
||||
percentage: number;
|
||||
totalSegments?: number;
|
||||
}
|
||||
|
||||
export const DashedCircle: React.FC<DashedCircleProps> = ({ center, color, percentage, totalSegments = 15 }) => {
|
||||
// Ensure percentage is between 0 and 100
|
||||
const validPercentage = Math.max(0, Math.min(100, percentage));
|
||||
|
||||
// Generate dashed segments for the circle
|
||||
const generateDashedCircle = () => {
|
||||
const segments = [];
|
||||
const angleIncrement = 360 / totalSegments;
|
||||
|
||||
for (let i = 0; i < totalSegments; i++) {
|
||||
// Calculate the angle for this segment (starting from top/12 o'clock position)
|
||||
const angle = i * angleIncrement - 90; // -90 adjusts to start from top center
|
||||
|
||||
// Calculate if this segment should be hidden based on percentage
|
||||
const segmentStartPercentage = (i / totalSegments) * 100;
|
||||
const isSegmentVisible = segmentStartPercentage >= validPercentage;
|
||||
|
||||
if (isSegmentVisible) {
|
||||
segments.push(
|
||||
<g key={i} transform={`translate(${center} ${center}) rotate(${angle})`}>
|
||||
<line x1={5.75} y1="0" x2={6.5} y2="0" stroke={color} strokeWidth={1.21} strokeLinecap="round" />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
};
|
||||
|
||||
return <g>{generateDashedCircle()}</g>;
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { EIconSize } from "@plane/constants";
|
||||
|
||||
export interface IStateGroupIcon {
|
||||
className?: string;
|
||||
color?: string;
|
||||
stateGroup: TStateGroups;
|
||||
height?: string;
|
||||
width?: string;
|
||||
size?: EIconSize;
|
||||
percentage?: number;
|
||||
}
|
||||
|
||||
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
@@ -11,9 +13,19 @@ export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "
|
||||
export const STATE_GROUP_COLORS: {
|
||||
[key in TStateGroups]: string;
|
||||
} = {
|
||||
backlog: "#d9d9d9",
|
||||
unstarted: "#3f76ff",
|
||||
started: "#f59e0b",
|
||||
completed: "#16a34a",
|
||||
cancelled: "#dc2626",
|
||||
backlog: "#60646C",
|
||||
unstarted: "#60646C",
|
||||
started: "#F59E0B",
|
||||
completed: "#46A758",
|
||||
cancelled: "#9AA4BC",
|
||||
};
|
||||
|
||||
export const STATE_GROUP_SIZES: {
|
||||
[key in EIconSize]: string;
|
||||
} = {
|
||||
[EIconSize.XS]: "10px",
|
||||
[EIconSize.SM]: "12px",
|
||||
[EIconSize.MD]: "14px",
|
||||
[EIconSize.LG]: "16px",
|
||||
[EIconSize.XL]: "18px",
|
||||
};
|
||||
|
||||
32
packages/ui/src/icons/state/progress-circle.tsx
Normal file
32
packages/ui/src/icons/state/progress-circle.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from "react";
|
||||
|
||||
interface ProgressCircleProps {
|
||||
center: number;
|
||||
radius: number;
|
||||
color: string;
|
||||
strokeWidth: number;
|
||||
circumference: number;
|
||||
dashOffset: number;
|
||||
}
|
||||
|
||||
export const ProgressCircle: React.FC<ProgressCircleProps> = ({
|
||||
center,
|
||||
radius,
|
||||
color,
|
||||
strokeWidth,
|
||||
circumference,
|
||||
dashOffset,
|
||||
}) => (
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={dashOffset}
|
||||
strokeLinecap="round"
|
||||
transform={`rotate(-90 ${center} ${center})`}
|
||||
/>
|
||||
);
|
||||
@@ -1,43 +1,65 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "../type";
|
||||
import { DashedCircle } from "./dashed-circle";
|
||||
import { ProgressCircle } from "./progress-circle";
|
||||
|
||||
// StateIcon component implementation
|
||||
export const StartedGroupIcon: React.FC<ISvgIcons> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#f39e1f",
|
||||
}) => (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 152.93 152.95"
|
||||
>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path
|
||||
fill={color}
|
||||
d="M77.74,0C35.63-.62.78,32.9,0,74.94c-.77,42.74,33,77.34,76.23,78A76.48,76.48,0,0,0,77.74,0ZM75.46,142.68a66.24,66.24,0,1,1,3-132.45c35.71,1,66.31,31.26,64.16,70.08A66.23,66.23,0,0,1,75.46,142.68Z"
|
||||
color = "#F59E0B",
|
||||
percentage = 100,
|
||||
}) => {
|
||||
// Ensure percentage is between 0 and 100
|
||||
const normalized =
|
||||
typeof percentage === "number"
|
||||
? percentage <= 1
|
||||
? percentage * 100 // treat 0-1 as fraction
|
||||
: percentage // already 0-100
|
||||
: 100; // fallback
|
||||
const validPercentage = Math.max(0, Math.min(100, normalized));
|
||||
|
||||
// SVG parameters
|
||||
const viewBoxSize = 16;
|
||||
const center = viewBoxSize / 2;
|
||||
const radius = 6;
|
||||
const strokeWidth = 1.5;
|
||||
|
||||
// Calculate the circumference of the circle
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const dashOffset = circumference * (1 - validPercentage / 100);
|
||||
const dashOffsetSmall = circumference * (1 - 100 / 100);
|
||||
|
||||
return (
|
||||
<svg width={width} height={height} viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`} className={className}>
|
||||
{/* Dashed background circle with segments that disappear with progress */}
|
||||
<DashedCircle center={center} radius={radius} color={color} percentage={validPercentage} />
|
||||
|
||||
{/* render smaller circle in the middle */}
|
||||
<circle
|
||||
cx={6}
|
||||
cy={6}
|
||||
r={3}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
transform={`rotate(-90 8 6)`}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={dashOffsetSmall}
|
||||
stroke={color}
|
||||
/>
|
||||
<path
|
||||
fill={color}
|
||||
d="M124.29,76.58a49.52,49.52,0,0,1-3.11,16.9c-.38,1-.77,1.27-1.81.78-2.15-1-4.34-1.92-6.56-2.72-1.3-.46-1.51-1-1-2.3a36.61,36.61,0,0,0,.64-23.77c-1-3.48-1.06-3.47,2.38-4.88,1.57-.65,3.15-1.27,4.68-2,.94-.44,1.34-.22,1.69.75A49.74,49.74,0,0,1,124.29,76.58Z"
|
||||
|
||||
{/* Solid progress circle */}
|
||||
<ProgressCircle
|
||||
center={center}
|
||||
radius={radius}
|
||||
color={color}
|
||||
strokeWidth={strokeWidth}
|
||||
circumference={circumference}
|
||||
dashOffset={dashOffset}
|
||||
/>
|
||||
<path
|
||||
fill={color}
|
||||
d="M94.65,32.63c-.1.22-.19.42-.27.63-1,2.5-2.08,5-3.09,7.51-.28.69-.55.89-1.37.59a37.3,37.3,0,0,0-26.82,0c-.91.34-1.15.08-1.46-.7-1-2.46-2-4.92-3.06-7.34-.42-.92-.07-1.18.69-1.46a47.66,47.66,0,0,1,34.43,0C94.06,32,94.68,32,94.65,32.63Z"
|
||||
/>
|
||||
<path
|
||||
fill={color}
|
||||
d="M28.72,76.67a48.27,48.27,0,0,1,3-17.13c.45-1.25.92-1.34,2-.83,2.25,1,4.56,2,6.87,2.87.86.34,1.05.67.71,1.58a36.85,36.85,0,0,0-.07,26.36c.36,1,.3,1.46-.75,1.86-2.38.9-4.72,1.88-7,2.92-1,.43-1.33.2-1.68-.76A46.76,46.76,0,0,1,28.72,76.67Z"
|
||||
/>
|
||||
<path
|
||||
fill={color}
|
||||
d="M76.37,124.22a48.11,48.11,0,0,1-16.91-3.08c-1.05-.38-1.26-.8-.79-1.82,1-2.31,2-4.66,2.93-7,.34-.87.69-1.06,1.61-.72a37.06,37.06,0,0,0,26.67,0c.75-.28,1.09-.23,1.39.55,1,2.56,2,5.13,3.18,7.65.49,1.08-.3,1.13-.86,1.34A46.53,46.53,0,0,1,76.37,124.22Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { BacklogGroupIcon } from "./backlog-group-icon";
|
||||
import { CancelledGroupIcon } from "./cancelled-group-icon";
|
||||
import { CompletedGroupIcon } from "./completed-group-icon";
|
||||
import { IStateGroupIcon, STATE_GROUP_COLORS, STATE_GROUP_SIZES } from "./helper";
|
||||
import { StartedGroupIcon } from "./started-group-icon";
|
||||
import { UnstartedGroupIcon } from "./unstarted-group-icon";
|
||||
import { IStateGroupIcon, STATE_GROUP_COLORS } from "./helper";
|
||||
|
||||
const iconComponents = {
|
||||
backlog: BacklogGroupIcon,
|
||||
@@ -19,17 +20,18 @@ export const StateGroupIcon: React.FC<IStateGroupIcon> = ({
|
||||
className = "",
|
||||
color,
|
||||
stateGroup,
|
||||
height = "12px",
|
||||
width = "12px",
|
||||
size = EIconSize.SM,
|
||||
percentage,
|
||||
}) => {
|
||||
const StateIconComponent = iconComponents[stateGroup] || UnstartedGroupIcon;
|
||||
|
||||
return (
|
||||
<StateIconComponent
|
||||
height={height}
|
||||
width={width}
|
||||
height={STATE_GROUP_SIZES[size]}
|
||||
width={STATE_GROUP_SIZES[size]}
|
||||
color={color ?? STATE_GROUP_COLORS[stateGroup]}
|
||||
className={`flex-shrink-0 ${className}`}
|
||||
percentage={percentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,51 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "../type";
|
||||
import { DashedCircle } from "./dashed-circle";
|
||||
import { ProgressCircle } from "./progress-circle";
|
||||
|
||||
// StateIcon component implementation
|
||||
export const UnstartedGroupIcon: React.FC<ISvgIcons> = ({
|
||||
width = "20",
|
||||
height = "20",
|
||||
className,
|
||||
color = "#3a3a3a",
|
||||
}) => (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="8" cy="8" r="7.4" stroke={color} strokeWidth="1.2" />
|
||||
color = "#F59E0B",
|
||||
percentage = 100,
|
||||
}) => {
|
||||
// Ensure percentage is between 0 and 100
|
||||
const normalized =
|
||||
typeof percentage === "number"
|
||||
? percentage <= 1
|
||||
? percentage * 100 // treat 0-1 as fraction
|
||||
: percentage // already 0-100
|
||||
: 100; // fallback
|
||||
const validPercentage = Math.max(0, Math.min(100, normalized));
|
||||
|
||||
// SVG parameters
|
||||
const viewBoxSize = 16;
|
||||
const center = viewBoxSize / 2;
|
||||
const radius = 6;
|
||||
const strokeWidth = 1.5;
|
||||
|
||||
// Calculate the circumference of the circle
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
// Calculate the dash offset based on percentage
|
||||
const dashOffset = circumference * (1 - validPercentage / 100);
|
||||
|
||||
return (
|
||||
<svg width={width} height={height} viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`} className={className}>
|
||||
<DashedCircle center={center} radius={radius} color={color} percentage={validPercentage} />
|
||||
|
||||
{/* Solid progress circle */}
|
||||
<ProgressCircle
|
||||
center={center}
|
||||
radius={radius}
|
||||
color={color}
|
||||
strokeWidth={strokeWidth}
|
||||
circumference={circumference}
|
||||
dashOffset={dashOffset}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export interface ISvgIcons extends React.SVGAttributes<SVGElement> {
|
||||
className?: string | undefined;
|
||||
percentage?: number;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// ui
|
||||
// plane imports
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useStates } from "@/hooks/store";
|
||||
@@ -26,7 +27,7 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div key={stateId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} height="12px" width="12px" />
|
||||
<StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} size={EIconSize.SM} />
|
||||
{stateDetails.name}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import isNil from "lodash/isNil";
|
||||
import { ContrastIcon } from "lucide-react";
|
||||
// types
|
||||
import { ISSUE_PRIORITIES } from "@plane/constants";
|
||||
import { EIconSize, ISSUE_PRIORITIES } from "@plane/constants";
|
||||
import {
|
||||
GroupByColumnTypes,
|
||||
IGroupByColumn,
|
||||
@@ -117,7 +117,7 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine
|
||||
name: state.name,
|
||||
icon: (
|
||||
<div className="h-3.5 w-3.5 rounded-full">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} width="14" height="14" />
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} size={EIconSize.MD} />
|
||||
</div>
|
||||
),
|
||||
payload: { state_id: state.id },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { Spinner, StateGroupIcon } from "@plane/ui";
|
||||
// store hooks
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
@@ -26,7 +27,12 @@ export const ChangeWorkItemStateList = observer((props: TChangeWorkItemStateList
|
||||
projectStates.map((state) => (
|
||||
<Command.Item key={state.id} onSelect={() => handleStateChange(state.id)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||
<StateGroupIcon
|
||||
stateGroup={state.group}
|
||||
color={state.color}
|
||||
size={EIconSize.LG}
|
||||
percentage={state?.order}
|
||||
/>
|
||||
<p>{state.name}</p>
|
||||
</div>
|
||||
<div>{state.id === currentStateId && <Check className="h-3 w-3" />}</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ArchiveX } from "lucide-react";
|
||||
// types
|
||||
import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel, EIconSize } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IProject } from "@plane/types";
|
||||
// ui
|
||||
@@ -42,7 +42,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} size={EIconSize.LG} />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
@@ -139,7 +139,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-between gap-2 px-5 py-4">
|
||||
<div className="ppy sm:py-10 flex w-full items-center justify-between gap-2 px-5 py-4">
|
||||
<div className="w-1/2 text-sm font-medium">
|
||||
{t("project_settings.automations.auto-close.auto_close_status")}
|
||||
</div>
|
||||
@@ -149,18 +149,12 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedOption ? (
|
||||
<StateGroupIcon
|
||||
stateGroup={selectedOption.group}
|
||||
color={selectedOption.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
<StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} size="lg" />
|
||||
) : currentDefaultState ? (
|
||||
<StateGroupIcon
|
||||
stateGroup={currentDefaultState.group}
|
||||
color={currentDefaultState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
size={EIconSize.LG}
|
||||
/>
|
||||
) : (
|
||||
<DoubleCircleIcon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
|
||||
@@ -98,7 +98,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
query: `${state?.name}`,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-3 w-3 flex-shrink-0" />
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
className="size-4 flex-shrink-0"
|
||||
percentage={state?.order}
|
||||
/>
|
||||
<span className="flex-grow truncate text-left">{state?.name}</span>
|
||||
</div>
|
||||
),
|
||||
@@ -179,7 +184,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
<StateGroupIcon
|
||||
stateGroup={selectedState?.group ?? "backlog"}
|
||||
color={selectedState?.color ?? "rgba(var(--color-text-300))"}
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
className="size-4 flex-shrink-0"
|
||||
percentage={selectedState?.order}
|
||||
/>
|
||||
)}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
|
||||
@@ -95,7 +95,12 @@ export const RecentIssue = observer((props: BlockProps) => {
|
||||
<div className="flex gap-4">
|
||||
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"}>
|
||||
<div>
|
||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-4 w-4 my-auto" />
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
className="h-4 w-4 my-auto"
|
||||
percentage={state?.order}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={issueDetails?.priority ?? "Priority"}>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { StateGroupIcon, Tag } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProjectInbox, useProjectState } from "@/hooks/store";
|
||||
@@ -30,7 +31,7 @@ export const InboxIssueAppliedFiltersState: FC = observer(() => {
|
||||
return (
|
||||
<div key={value} className="relative flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<div className="w-3 h-3 flex-shrink-0 relative flex justify-center items-center overflow-hidden">
|
||||
<StateGroupIcon color={optionDetail.color} stateGroup={optionDetail.group} height="12px" width="12px" />
|
||||
<StateGroupIcon color={optionDetail.color} stateGroup={optionDetail.group} size={EIconSize.SM} />
|
||||
</div>
|
||||
<div className="text-xs truncate">{optionDetail?.name}</div>
|
||||
<div
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { IState } from "@plane/types";
|
||||
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||
// components
|
||||
@@ -55,7 +56,14 @@ export const FilterState: FC<Props> = observer((props) => {
|
||||
key={state?.id}
|
||||
isChecked={filterValue?.includes(state?.id) ? true : false}
|
||||
onClick={() => handleInboxIssueFilters("state", handleFilterValue(state.id))}
|
||||
icon={<StateGroupIcon color={state.color} stateGroup={state.group} height="12px" width="12px" />}
|
||||
icon={
|
||||
<StateGroupIcon
|
||||
color={state.color}
|
||||
stateGroup={state.group}
|
||||
size={EIconSize.SM}
|
||||
percentage={state?.order}
|
||||
/>
|
||||
}
|
||||
title={state.name}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
|
||||
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { TStateGroups } from "@plane/types";
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
|
||||
@@ -19,7 +20,7 @@ export const AppliedStateGroupFilters: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
{values.map((stateGroup) => (
|
||||
<div key={stateGroup} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<StateGroupIcon stateGroup={stateGroup as TStateGroups} height="12px" width="12px" />
|
||||
<StateGroupIcon stateGroup={stateGroup as TStateGroups} size={EIconSize.SM} />
|
||||
{stateGroup}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
import { IState } from "@plane/types";
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// types
|
||||
|
||||
@@ -27,7 +28,12 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div key={stateId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} height="12px" width="12px" />
|
||||
<StateGroupIcon
|
||||
color={stateDetails.color}
|
||||
stateGroup={stateDetails.group}
|
||||
size={EIconSize.SM}
|
||||
percentage={stateDetails?.order}
|
||||
/>
|
||||
{stateDetails.name}
|
||||
{editable && (
|
||||
<button
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import { observer } from "mobx-react";
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { IState } from "@plane/types";
|
||||
// components
|
||||
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||
@@ -56,7 +57,14 @@ export const FilterState: React.FC<Props> = observer((props) => {
|
||||
key={state.id}
|
||||
isChecked={appliedFilters?.includes(state.id) ? true : false}
|
||||
onClick={() => handleUpdate(state.id)}
|
||||
icon={<StateGroupIcon stateGroup={state.group} color={state.color} />}
|
||||
icon={
|
||||
<StateGroupIcon
|
||||
stateGroup={state.group}
|
||||
color={state.color}
|
||||
size={EIconSize.MD}
|
||||
percentage={state?.order}
|
||||
/>
|
||||
}
|
||||
title={state.name}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -11,7 +11,7 @@ import uniq from "lodash/uniq";
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import { ContrastIcon } from "lucide-react";
|
||||
// plane types
|
||||
import { EIssuesStoreType, ISSUE_PRIORITIES, STATE_GROUPS } from "@plane/constants";
|
||||
import { EIconSize, EIssuesStoreType, ISSUE_PRIORITIES, STATE_GROUPS } from "@plane/constants";
|
||||
import {
|
||||
GroupByColumnTypes,
|
||||
IGroupByColumn,
|
||||
@@ -198,8 +198,8 @@ const getStateColumns = (): IGroupByColumn[] | undefined => {
|
||||
id: state.id,
|
||||
name: state.name,
|
||||
icon: (
|
||||
<div className="h-3.5 w-3.5 rounded-full">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} width="14" height="14" />
|
||||
<div className="size-4 rounded-full">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} size={EIconSize.LG} percentage={state.order} />
|
||||
</div>
|
||||
),
|
||||
payload: { state_id: state.id },
|
||||
@@ -213,8 +213,8 @@ const getStateGroupColumns = (): IGroupByColumn[] => {
|
||||
id: stateGroup.key,
|
||||
name: stateGroup.label,
|
||||
icon: (
|
||||
<div className="h-3.5 w-3.5 rounded-full">
|
||||
<StateGroupIcon stateGroup={stateGroup.key} width="14" height="14" />
|
||||
<div className="size-4 rounded-full">
|
||||
<StateGroupIcon stateGroup={stateGroup.key} size={EIconSize.LG} />
|
||||
</div>
|
||||
),
|
||||
payload: {},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FC, useState, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronDown, Plus } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
@@ -74,7 +75,7 @@ export const GroupItem: FC<TGroupItem> = observer((props) => {
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-6 h-6 rounded flex justify-center items-center overflow-hidden">
|
||||
<StateGroupIcon stateGroup={groupKey} height="16px" width="16px" />
|
||||
<StateGroupIcon stateGroup={groupKey} size={EIconSize.XL} />
|
||||
</div>
|
||||
<div className="text-base font-medium text-custom-text-200 capitalize px-1">{groupKey}</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,11 @@ import { SetStateAction } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { GripVertical, Pencil } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { IState, TStateOperationsCallbacks } from "@plane/types";
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// local imports
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
import { StateDelete, StateMarksAsDefault } from "./options";
|
||||
|
||||
type TBaseStateItemTitleProps = {
|
||||
@@ -28,6 +30,11 @@ export type TStateItemTitleProps = TEnabledStateItemTitleProps | TDisabledStateI
|
||||
|
||||
export const StateItemTitle = observer((props: TStateItemTitleProps) => {
|
||||
const { stateCount, setUpdateStateModal, disabled, state, shouldShowDescription = true } = props;
|
||||
// store hooks
|
||||
const { getStatePercentageInGroup } = useProjectState();
|
||||
// derived values
|
||||
const statePercentage = getStatePercentageInGroup(state.id);
|
||||
const percentage = statePercentage ? statePercentage / 100 : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full justify-between">
|
||||
@@ -40,7 +47,7 @@ export const StateItemTitle = observer((props: TStateItemTitleProps) => {
|
||||
)}
|
||||
{/* state icon */}
|
||||
<div className="flex-shrink-0">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} className={"size-3.5"} />
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} size={EIconSize.XL} percentage={percentage} />
|
||||
</div>
|
||||
{/* state title and description */}
|
||||
<div className="text-sm px-2 min-h-5">
|
||||
|
||||
@@ -45,6 +45,8 @@ export interface IStateStore {
|
||||
stateId: string,
|
||||
payload: Partial<IState>
|
||||
) => Promise<void>;
|
||||
|
||||
getStatePercentageInGroup: (stateId: string | null | undefined) => number | undefined;
|
||||
}
|
||||
|
||||
export class StateStore implements IStateStore {
|
||||
@@ -303,4 +305,27 @@ export class StateStore implements IStateStore {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the percentage position of a state within its group based on sequence
|
||||
* @param stateId The ID of the state to find the percentage for
|
||||
* @returns The percentage position of the state in its group (0-100), or -1 if not found
|
||||
*/
|
||||
getStatePercentageInGroup = computedFn((stateId: string | null | undefined) => {
|
||||
if (!stateId || !this.stateMap[stateId]) return -1;
|
||||
|
||||
const state = this.stateMap[stateId];
|
||||
const group = state.group;
|
||||
|
||||
if (!group || !this.groupedProjectStates || !this.groupedProjectStates[group]) return -1;
|
||||
|
||||
// Get all states in the same group
|
||||
const statesInGroup = this.groupedProjectStates[group];
|
||||
const stateIndex = statesInGroup.findIndex((s) => s.id === stateId);
|
||||
|
||||
if (stateIndex === -1) return undefined;
|
||||
|
||||
// Calculate percentage: ((index + 1) / totalLength) * 100
|
||||
return ((stateIndex + 1) / statesInGroup.length) * 100;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user