Compare commits

..
Author SHA1 Message Date
pablohashescobar 6c6aa3b0d5 chore: separate out project feature model 2024-01-29 14:13:42 +05:30
67 changed files with 676 additions and 779 deletions
@@ -8,6 +8,7 @@ from plane.db.models import (
WorkspaceMember,
State,
Estimate,
ProjectFeature,
)
from .base import BaseSerializer
@@ -78,6 +79,7 @@ class ProjectSerializer(BaseSerializer):
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
)
_ = ProjectIdentifier.objects.create(
name=project.identifier,
project=project,
+6 -1
View File
@@ -11,7 +11,6 @@ from rest_framework.serializers import ValidationError
from plane.db.models import (
Workspace,
Project,
ProjectFavorite,
ProjectMember,
ProjectDeployBoard,
State,
@@ -19,6 +18,7 @@ from plane.db.models import (
Module,
IssueProperty,
Inbox,
ProjectFeature,
)
from plane.app.permissions import ProjectBasePermission
from plane.api.serializers import ProjectSerializer
@@ -149,6 +149,11 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
if serializer.is_valid():
serializer.save()
# features
_ = ProjectFeature.objects.create(
project_id=serializer.data["id"]
)
# Add the user as Administrator to the project
project_member = ProjectMember.objects.create(
project_id=serializer.data["id"],
@@ -21,6 +21,7 @@ from .workspace import (
)
from .project import (
ProjectSerializer,
ProjectFeatureSerializer,
ProjectListSerializer,
ProjectDetailSerializer,
ProjectMemberSerializer,
-4
View File
@@ -304,7 +304,6 @@ class IssueRelationSerializer(BaseSerializer):
sequence_id = serializers.IntegerField(
source="related_issue.sequence_id", read_only=True
)
name = serializers.CharField(source="related_issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta:
@@ -314,7 +313,6 @@ class IssueRelationSerializer(BaseSerializer):
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
@@ -330,7 +328,6 @@ class RelatedIssueSerializer(BaseSerializer):
sequence_id = serializers.IntegerField(
source="issue.sequence_id", read_only=True
)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta:
@@ -340,7 +337,6 @@ class RelatedIssueSerializer(BaseSerializer):
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
@@ -16,6 +16,7 @@ from plane.db.models import (
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
ProjectFeature,
)
@@ -47,6 +48,8 @@ class ProjectSerializer(BaseSerializer):
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
)
# Create identifiers
_ = ProjectIdentifier.objects.create(
name=project.identifier,
project=project,
@@ -237,3 +240,14 @@ class ProjectPublicMemberSerializer(BaseSerializer):
"project",
"member",
]
class ProjectFeatureSerializer(BaseSerializer):
class Meta:
model = ProjectFeature
fields = "__all__"
read_only_fields = [
"project",
"workspace",
]
+6
View File
@@ -14,6 +14,7 @@ from plane.app.views import (
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
UserProjectRolesEndpoint,
ProjectFeatureEndpoint,
)
@@ -175,4 +176,9 @@ urlpatterns = [
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-features/",
ProjectFeatureEndpoint.as_view(),
name="project-feature",
),
]
+1
View File
@@ -12,6 +12,7 @@ from .project import (
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
UserProjectRolesEndpoint,
ProjectFeatureEndpoint,
)
from .user import (
UserEndpoint,
+43 -3
View File
@@ -37,6 +37,7 @@ from plane.app.serializers import (
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectMemberRoleSerializer,
ProjectFeatureSerializer,
)
from plane.app.permissions import (
@@ -62,6 +63,7 @@ from plane.db.models import (
Inbox,
ProjectDeployBoard,
IssueProperty,
ProjectFeature,
)
from plane.bgtasks.project_invitation_task import project_invitation
@@ -203,6 +205,10 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
if serializer.is_valid():
serializer.save()
_ = ProjectFeature.objects.create(
project_id=serializer.data["id"]
)
# Add the user as Administrator to the project
project_member = ProjectMember.objects.create(
project_id=serializer.data["id"],
@@ -686,9 +692,14 @@ class ProjectMemberViewSet(BaseViewSet):
)
bulk_project_members = []
member_roles = {member.get("member_id"): member.get("role") for member in members}
member_roles = {
member.get("member_id"): member.get("role") for member in members
}
# Update roles in the members array based on the member_roles dictionary
for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]):
for project_member in ProjectMember.objects.filter(
project_id=project_id,
member_id__in=[member.get("member_id") for member in members],
):
project_member.role = member_roles[str(project_member.member_id)]
project_member.is_active = True
bulk_project_members.append(project_member)
@@ -734,7 +745,10 @@ class ProjectMemberViewSet(BaseViewSet):
bulk_issue_props, batch_size=10, ignore_conflicts=True
)
project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members])
project_members = ProjectMember.objects.filter(
project_id=project_id,
member_id__in=[member.get("member_id") for member in members],
)
serializer = ProjectMemberRoleSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -1138,3 +1152,29 @@ class UserProjectRolesEndpoint(BaseAPIView):
for member in project_members
}
return Response(project_members, status=status.HTTP_200_OK)
class ProjectFeatureEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
def get(self, request, slug, project_id):
project_feature = ProjectFeature.objects.get(
workspace__slug=slug, project_id=project_id
)
serializer = ProjectFeatureSerializer(project_feature)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id):
project_feature = ProjectFeature.objects.get(
workspace__slug=slug,
project_id=project_id,
)
serializer = ProjectFeatureSerializer(
project_feature, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -0,0 +1,120 @@
# Generated by Django 4.2.7 on 2024-01-29 07:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
def create_project_preferences(apps, schema_editor):
ProjectFeature = apps.get_model("db", "ProjectFeature")
Project = apps.get_model("db", "Project")
ProjectFeature.objects.bulk_create(
[
ProjectFeature(
project_id=project.get("id"),
workspace_id=project.get("workspace_id"),
cycles=project.get("cycle_view"),
modules=project.get("module_view"),
views=project.get("issue_views_view"),
pages=project.get("page_view"),
inbox=project.get("inbox_view"),
)
for project in Project.objects.values(
"id",
"workspace_id",
"cycle_view",
"inbox_view",
"issue_views_view",
"page_view",
"module_view",
)
],
batch_size=100,
)
class Migration(migrations.Migration):
dependencies = [
("db", "0057_auto_20240122_0901"),
]
operations = [
migrations.CreateModel(
name="ProjectFeature",
fields=[
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("modules", models.BooleanField(default=True)),
("cycles", models.BooleanField(default=True)),
("views", models.BooleanField(default=True)),
("pages", models.BooleanField(default=True)),
("inbox", models.BooleanField(default=False)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"project",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="features",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="project_features",
to="db.workspace",
),
),
],
options={
"verbose_name": "Project Feature",
"verbose_name_plural": "Project Features",
"db_table": "project_features",
"ordering": ("-created_at",),
},
),
migrations.RunPython(create_project_preferences),
]
+1
View File
@@ -22,6 +22,7 @@ from .project import (
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
ProjectFeature,
)
from .issue import (
+30 -5
View File
@@ -89,11 +89,6 @@ class Project(BaseModel):
)
emoji = models.CharField(max_length=255, null=True, blank=True)
icon_prop = models.JSONField(null=True)
module_view = models.BooleanField(default=True)
cycle_view = models.BooleanField(default=True)
issue_views_view = models.BooleanField(default=True)
page_view = models.BooleanField(default=True)
inbox_view = models.BooleanField(default=False)
cover_image = models.URLField(blank=True, null=True, max_length=800)
estimate = models.ForeignKey(
"db.Estimate",
@@ -146,6 +141,36 @@ class ProjectBaseModel(BaseModel):
super(ProjectBaseModel, self).save(*args, **kwargs)
class ProjectFeature(BaseModel):
workspace = models.ForeignKey(
"db.Workspace",
on_delete=models.CASCADE,
related_name="project_features",
)
project = models.OneToOneField(
Project, on_delete=models.CASCADE, related_name="features"
)
modules = models.BooleanField(default=False)
cycles = models.BooleanField(default=False)
views = models.BooleanField(default=False)
pages = models.BooleanField(default=True)
inbox = models.BooleanField(default=False)
def save(self, *args, **kwargs):
self.workspace = self.project.workspace
super(ProjectFeature, self).save(*args, **kwargs)
def __str__(self):
"""Return feature of the project"""
return f"{self.project.name} <{self.id}>"
class Meta:
verbose_name = "Project Feature"
verbose_name_plural = "Project Features"
db_table = "project_features"
ordering = ("-created_at",)
class ProjectMemberInvite(ProjectBaseModel):
email = models.CharField(max_length=255)
accepted = models.BooleanField(default=False)
+1 -1
View File
@@ -30,7 +30,7 @@ openpyxl==3.1.2
beautifulsoup4==4.12.2
dj-database-url==2.1.0
posthog==3.0.2
cryptography==41.0.6
cryptography==41.0.5
lxml==4.9.3
boto3==1.28.40
@@ -65,7 +65,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
);
},
}}
margin={{ top: 20, right: 20 }}
margin={{ top: 20 }}
theme={{
axis: {},
}}
@@ -6,76 +6,54 @@ import emptyGraph from "public/empty-state/empty_graph.svg";
import { IDefaultAnalyticsResponse } from "@plane/types";
// constants
import { MONTHS_LIST } from "constants/calendar";
import { useEffect, useState } from "react";
type Props = {
defaultAnalytics: IDefaultAnalyticsResponse;
};
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => {
// states
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
// event listener to update the width
window.addEventListener("resize", () => {
if (width <= 768 && window.innerWidth > 768) {
setWidth(window.innerWidth);
} else if (width >= 768 && window.innerWidth < 768) {
setWidth(window.innerWidth);
}
});
}, []);
return (
<div className="rounded-[10px] border border-custom-border-200 py-3">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
<LineGraph
data={[
{
id: "issues_closed",
color: "rgb(var(--color-primary-100))",
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
x: month.shortTitle,
y:
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))
?.count || 0,
})),
},
]}
axisBottom={{
// if width is less than 768px, rotate the x-axis labels
...(width < 768 && {
tickRotation: -45,
}),
}}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
margin={{ top: 20, right: 20 }}
enableSlices="x"
sliceTooltip={(datum) => (
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
{datum.slice.points[0].data.yFormatted}
<span className="text-custom-text-200"> issues closed in </span>
{datum.slice.points[0].data.xFormatted}
</div>
)}
theme={{
background: "rgb(var(--color-background-100))",
}}
enableArea
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="rounded-[10px] border border-custom-border-200 py-3">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
<LineGraph
data={[
{
id: "issues_closed",
color: "rgb(var(--color-primary-100))",
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
x: month.shortTitle,
y:
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))?.count ||
0,
})),
},
]}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
margin={{ top: 20 }}
enableSlices="x"
sliceTooltip={(datum) => (
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
{datum.slice.points[0].data.yFormatted}
<span className="text-custom-text-200"> issues closed in </span>
{datum.slice.points[0].data.xFormatted}
</div>
)}
theme={{
background: "rgb(var(--color-background-100))",
}}
enableArea
/>
) : (
<div className="px-7 py-4">
<ProfileEmptyState
title="No Data yet"
description="Close issues to view analysis of the same in the form of a graph."
image={emptyGraph}
/>
) : (
<div className="px-7 py-4">
<ProfileEmptyState
title="No Data yet"
description="Close issues to view analysis of the same in the form of a graph."
image={emptyGraph}
/>
</div>
)}
</div>
);
};
</div>
)}
</div>
);
@@ -2,7 +2,6 @@ import { MouseEvent } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useTheme } from "next-themes";
// hooks
import { useCycle, useIssues, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
@@ -44,7 +43,6 @@ interface IActiveCycleDetails {
export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props) => {
// props
const { workspaceSlug, projectId } = props;
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser } = useUser();
const {
@@ -78,9 +76,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
);
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("cycle", "active", isLightMode);
const emptyStateImage = getEmptyStateImagePath("cycle", "active", currentUser?.theme.theme === "light");
if (!activeCycle && isLoading)
return (
+1 -6
View File
@@ -1,6 +1,5 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useUser } from "hooks/store";
// components
@@ -19,15 +18,11 @@ export interface ICyclesBoard {
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser } = useUser();
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode);
const emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light");
return (
<>
+1 -6
View File
@@ -1,6 +1,5 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useUser } from "hooks/store";
// components
@@ -20,15 +19,11 @@ export interface ICyclesList {
export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycleIds, filter, workspaceSlug, projectId } = props;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser } = useUser();
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode);
const emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light");
return (
<>
+90 -113
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form";
@@ -46,11 +46,6 @@ type Props = {
handleClose: () => void;
};
const defaultValues: Partial<ICycle> = {
start_date: null,
end_date: null,
};
// services
const cycleService = new CycleService();
@@ -59,9 +54,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { cycleId, handleClose } = props;
// states
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// refs
const startDateButtonRef = useRef<HTMLButtonElement | null>(null);
const endDateButtonRef = useRef<HTMLButtonElement | null>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
@@ -78,6 +70,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast();
const defaultValues: Partial<ICycle> = {
start_date: new Date().toString(),
end_date: new Date().toString(),
};
const { setValue, reset, watch } = useForm({
defaultValues,
});
@@ -123,9 +120,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const handleStartDateChange = async (date: string) => {
setValue("start_date", date);
if (!watch("end_date") || watch("end_date") === "") endDateButtonRef.current?.click();
if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") {
if (!isDateGreaterThanToday(`${watch("end_date")}`)) {
setToastAlert({
@@ -133,7 +127,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
title: "Error!",
message: "Unable to create cycle in past date. Please enter a valid date.",
});
reset({ ...cycleDetails });
return;
}
@@ -154,6 +147,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
title: "Success!",
message: "Cycle updated successfully.",
});
return;
} else {
setToastAlert({
type: "error",
@@ -161,10 +155,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
return;
}
reset({ ...cycleDetails });
return;
}
const isDateValid = await dateChecker({
@@ -189,7 +181,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
reset({ ...cycleDetails });
}
}
};
@@ -197,8 +188,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const handleEndDateChange = async (date: string) => {
setValue("end_date", date);
if (!watch("start_date") || watch("start_date") === "") startDateButtonRef.current?.click();
if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") {
if (!isDateGreaterThanToday(`${watch("end_date")}`)) {
setToastAlert({
@@ -206,7 +195,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
title: "Error!",
message: "Unable to create cycle in past date. Please enter a valid date.",
});
reset({ ...cycleDetails });
return;
}
@@ -227,6 +215,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
title: "Success!",
message: "Cycle updated successfully.",
});
return;
} else {
setToastAlert({
type: "error",
@@ -234,9 +223,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
return;
}
reset({ ...cycleDetails });
return;
}
const isDateValid = await dateChecker({
@@ -261,7 +249,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
reset({ ...cycleDetails });
}
}
};
@@ -400,56 +387,50 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<CalendarClock className="h-4 w-4" />
<span className="text-base">Start date</span>
<span className="text-base">Start Date</span>
</div>
<div className="relative flex w-1/2 items-center rounded-sm">
<Popover className="flex h-full w-full items-center justify-center rounded-lg">
{({ close }) => (
<>
<Popover.Button
ref={startDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("start_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(startDate) ?? "No date selected"}
</span>
</Popover.Button>
<Popover.Button
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("start_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(startDate) ?? "No date selected"}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
onChange={(val) => {
if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_START_DATE_BUTTON");
handleStartDateChange(val);
close();
}
}}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("end_date")}`)}
selectsStart={watch("end_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
)}
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
onChange={(val) => {
if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_START_DATE_BUTTON");
handleStartDateChange(val);
}
}}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("end_date")}`)}
selectsStart={watch("end_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</Popover>
</div>
</div>
@@ -457,56 +438,52 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<CalendarCheck2 className="h-4 w-4" />
<span className="text-base">Target date</span>
<span className="text-base">Target Date</span>
</div>
<div className="relative flex w-1/2 items-center rounded-sm">
<Popover className="flex h-full w-full items-center justify-center rounded-lg">
{({ close }) => (
<>
<Popover.Button
ref={endDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
<>
<Popover.Button
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("end_date") ? "" : "text-custom-text-400"
}`}
disabled={isCompleted || !isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("end_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(endDate) ?? "No date selected"}
</span>
</Popover.Button>
{renderFormattedDate(endDate) ?? "No date selected"}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => {
if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_END_DATE_BUTTON");
handleEndDateChange(val);
close();
}
}}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
)}
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => {
if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_END_DATE_BUTTON");
handleEndDateChange(val);
}
}}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
</Popover>
</div>
</div>
@@ -50,11 +50,11 @@ export const DashboardWidgets = observer(() => {
// if the widget is full width, return it in a 2 column grid
if (widget.fullWidth)
return (
<div key={key} className="lg:col-span-2">
<div className="lg:col-span-2">
<WidgetComponent dashboardId={homeDashboardId} workspaceSlug={workspaceSlug} />
</div>
);
else return <WidgetComponent key={key} dashboardId={homeDashboardId} workspaceSlug={workspaceSlug} />;
else return <WidgetComponent dashboardId={homeDashboardId} workspaceSlug={workspaceSlug} />;
})}
</div>
);
@@ -99,7 +99,7 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
</div>
<Tab.Panels as="div" className="h-full">
{ISSUES_TABS_LIST.map((tab) => (
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col">
<Tab.Panel as="div" className="h-full flex flex-col">
<WidgetIssuesList
issues={widgetStats.issues}
tab={tab.key}
@@ -57,7 +57,7 @@ const ProjectListItem: React.FC<ProjectListItemProps> = observer((props) => {
<div className="mt-2">
<AvatarGroup>
{projectDetails.members?.map((member) => (
<Avatar key={member.member_id} src={member.member__avatar} name={member.member__display_name} />
<Avatar src={member.member__avatar} name={member.member__display_name} />
))}
</AvatarGroup>
</div>
-9
View File
@@ -30,7 +30,6 @@ type ButtonProps = {
hideIcon: boolean;
hideText?: boolean;
dropdownArrow: boolean;
isActive?: boolean;
dropdownArrowClassName: string;
placeholder: string;
tooltip: boolean;
@@ -52,7 +51,6 @@ const BorderButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
tooltip,
} = props;
@@ -62,7 +60,6 @@ const BorderButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
@@ -114,7 +111,6 @@ const TransparentButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
tooltip,
} = props;
@@ -124,7 +120,6 @@ const TransparentButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
@@ -273,7 +268,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
@@ -285,7 +279,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "background-with-text" ? (
@@ -317,7 +310,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
@@ -329,7 +321,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : null}
+1 -20
View File
@@ -2,7 +2,7 @@ import React, { useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import DatePicker from "react-datepicker";
import { usePopper } from "react-popper";
import { Calendar, CalendarDays, X } from "lucide-react";
import { CalendarDays, X } from "lucide-react";
// hooks
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
@@ -23,7 +23,6 @@ type Props = TDropdownProps & {
onChange: (val: Date | null) => void;
value: Date | string | null;
closeOnSelect?: boolean;
showPlaceholderIcon?: boolean;
};
type ButtonProps = {
@@ -34,11 +33,9 @@ type ButtonProps = {
isClearable: boolean;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
onClear: () => void;
placeholder: string;
tooltip: boolean;
showPlaceholderIcon?: boolean;
};
const BorderButton = (props: ButtonProps) => {
@@ -50,7 +47,6 @@ const BorderButton = (props: ButtonProps) => {
isClearable,
hideIcon = false,
hideText = false,
isActive = false,
onClear,
placeholder,
tooltip,
@@ -65,7 +61,6 @@ const BorderButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
@@ -136,11 +131,9 @@ const TransparentButton = (props: ButtonProps) => {
isClearable,
hideIcon = false,
hideText = false,
isActive = false,
onClear,
placeholder,
tooltip,
showPlaceholderIcon = false,
} = props;
return (
@@ -152,16 +145,11 @@ const TransparentButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && icon}
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
{showPlaceholderIcon && !date && (
<Calendar className="h-2.5 w-2.5 flex-shrink-0 hidden group-hover:inline text-custom-text-400" />
)}
{isClearable && (
<X
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
@@ -195,7 +183,6 @@ export const DateDropdown: React.FC<Props> = (props) => {
placement,
tabIndex,
tooltip = false,
showPlaceholderIcon = false,
value,
} = props;
const [isOpen, setIsOpen] = useState(false);
@@ -259,7 +246,6 @@ export const DateDropdown: React.FC<Props> = (props) => {
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
@@ -272,7 +258,6 @@ export const DateDropdown: React.FC<Props> = (props) => {
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
@@ -311,9 +296,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
isActive={isOpen}
tooltip={tooltip}
showPlaceholderIcon={showPlaceholderIcon}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
@@ -325,10 +308,8 @@ export const DateDropdown: React.FC<Props> = (props) => {
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
isActive={isOpen}
tooltip={tooltip}
hideText
showPlaceholderIcon={showPlaceholderIcon}
/>
) : null}
</button>
-9
View File
@@ -31,7 +31,6 @@ type ButtonProps = {
dropdownArrowClassName: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
placeholder: string;
tooltip: boolean;
};
@@ -52,7 +51,6 @@ const BorderButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
tooltip,
} = props;
@@ -66,7 +64,6 @@ const BorderButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
@@ -126,7 +123,6 @@ const TransparentButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
tooltip,
} = props;
@@ -140,7 +136,6 @@ const TransparentButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
@@ -281,7 +276,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
@@ -293,7 +287,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "background-with-text" ? (
@@ -325,7 +318,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
@@ -337,7 +329,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : null}
+9 -26
View File
@@ -14,7 +14,6 @@ type ButtonProps = {
placeholder: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
tooltip: boolean;
userIds: string | string[] | null;
};
@@ -51,7 +50,6 @@ export const BorderButton = observer((props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
userIds,
tooltip,
@@ -59,7 +57,7 @@ export const BorderButton = observer((props: ButtonProps) => {
// store hooks
const { getUserDetails } = useMember();
const isArray = Array.isArray(userIds);
const isMultiple = Array.isArray(userIds);
return (
<Tooltip
@@ -70,18 +68,13 @@ export const BorderButton = observer((props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
{!hideText && (
<span className="flex-grow truncate text-sm leading-5">
{isArray && userIds.length > 0
? userIds.length === 1
? getUserDetails(userIds[0])?.display_name
: ""
: placeholder}
<span className="flex-grow truncate">
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
</span>
)}
{dropdownArrow && (
@@ -106,7 +99,7 @@ export const BackgroundButton = observer((props: ButtonProps) => {
// store hooks
const { getUserDetails } = useMember();
const isArray = Array.isArray(userIds);
const isMultiple = Array.isArray(userIds);
return (
<Tooltip
@@ -122,12 +115,8 @@ export const BackgroundButton = observer((props: ButtonProps) => {
>
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
{!hideText && (
<span className="flex-grow truncate text-sm leading-5">
{isArray && userIds.length > 0
? userIds.length === 1
? getUserDetails(userIds[0])?.display_name
: ""
: placeholder}
<span className="flex-grow truncate">
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
</span>
)}
{dropdownArrow && (
@@ -145,7 +134,6 @@ export const TransparentButton = observer((props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
placeholder,
userIds,
tooltip,
@@ -153,7 +141,7 @@ export const TransparentButton = observer((props: ButtonProps) => {
// store hooks
const { getUserDetails } = useMember();
const isArray = Array.isArray(userIds);
const isMultiple = Array.isArray(userIds);
return (
<Tooltip
@@ -164,18 +152,13 @@ export const TransparentButton = observer((props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
{!hideText && (
<span className="flex-grow truncate text-sm leading-5">
{isArray && userIds.length > 0
? userIds.length === 1
? getUserDetails(userIds[0])?.display_name
: ""
: placeholder}
<span className="flex-grow truncate">
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
</span>
)}
{dropdownArrow && (
@@ -147,7 +147,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
@@ -158,7 +157,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
@@ -191,7 +189,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
@@ -202,7 +199,6 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
-9
View File
@@ -38,7 +38,6 @@ type ButtonProps = {
dropdownArrowClassName: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
module: IModule | null;
placeholder: string;
tooltip: boolean;
@@ -51,7 +50,6 @@ const BorderButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
module,
placeholder,
tooltip,
@@ -62,7 +60,6 @@ const BorderButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{ "bg-custom-background-80": isActive },
className
)}
>
@@ -113,7 +110,6 @@ const TransparentButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
module,
placeholder,
tooltip,
@@ -124,7 +120,6 @@ const TransparentButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{ "bg-custom-background-80": isActive },
className
)}
>
@@ -272,7 +267,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
@@ -284,7 +278,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "background-with-text" ? (
@@ -316,7 +309,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
@@ -328,7 +320,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
hideIcon={hideIcon}
hideText
placeholder={placeholder}
isActive={isOpen}
tooltip={tooltip}
/>
) : null}
+1 -12
View File
@@ -31,7 +31,6 @@ type ButtonProps = {
dropdownArrowClassName: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
highlightUrgent: boolean;
priority: TIssuePriorities;
tooltip: boolean;
@@ -182,7 +181,6 @@ const TransparentButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
highlightUrgent,
priority,
tooltip,
@@ -209,7 +207,6 @@ const TransparentButton = (props: ButtonProps) => {
"px-0.5": hideText,
// highlight the whole button if text is hidden and priority is urgent
"bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent,
"bg-custom-background-80": isActive,
},
className
)}
@@ -315,13 +312,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn(
"h-full",
{
"bg-custom-background-80": isOpen,
},
className
)}
className={cn("h-full", className)}
value={value}
onChange={onChange}
disabled={disabled}
@@ -411,7 +402,6 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
@@ -424,7 +414,6 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
-13
View File
@@ -30,7 +30,6 @@ type ButtonProps = {
dropdownArrowClassName: string;
hideIcon?: boolean;
hideText?: boolean;
isActive?: boolean;
state: IState | undefined;
tooltip: boolean;
};
@@ -42,7 +41,6 @@ const BorderButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
state,
tooltip,
} = props;
@@ -52,9 +50,6 @@ const BorderButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
{
"bg-custom-background-80": isActive,
},
className
)}
>
@@ -116,7 +111,6 @@ const TransparentButton = (props: ButtonProps) => {
dropdownArrowClassName,
hideIcon = false,
hideText = false,
isActive = false,
state,
tooltip,
} = props;
@@ -126,9 +120,6 @@ const TransparentButton = (props: ButtonProps) => {
<div
className={cn(
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
{
"bg-custom-background-80": isActive,
},
className
)}
>
@@ -260,7 +251,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "border-without-text" ? (
@@ -270,7 +260,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
@@ -300,7 +289,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
/>
) : buttonVariant === "transparent-without-text" ? (
@@ -310,7 +298,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
isActive={isOpen}
tooltip={tooltip}
hideText
/>
@@ -64,7 +64,6 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
{
"cursor-not-allowed": disabled,
"hover:bg-custom-background-80": !disabled,
"bg-custom-background-80": isParentIssueModalOpen,
},
className
)}
@@ -73,20 +72,15 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
>
{issue.parent_id && parentIssue ? (
<div className="flex items-center gap-1 bg-green-500/20 text-green-700 rounded px-1.5 py-1">
<Tooltip tooltipHeading="Title" tooltipContent={parentIssue.name}>
<Link
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium mt-0.5"
onClick={(e) => e.stopPropagation()}
>
{parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id}
</Link>
</Tooltip>
<Link
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
className="text-xs font-medium"
>
{parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id}
</Link>
{!disabled && (
<Tooltip tooltipContent="Remove" position="bottom">
<Tooltip tooltipContent="Remove">
<span
onClick={(e) => {
e.preventDefault();
@@ -102,15 +96,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
) : (
<span className="text-sm text-custom-text-400">Add parent issue</span>
)}
{!disabled && (
<span
className={cn("p-1 flex-shrink-0 opacity-0 group-hover:opacity-100", {
"text-custom-text-400": !issue.parent_id && !parentIssue,
})}
>
<Pencil className="h-2.5 w-2.5 flex-shrink-0" />
</span>
)}
{!disabled && <Pencil className="h-4 w-4 flex-shrink-0 hidden group-hover:inline" />}
</button>
</>
);
@@ -1,5 +1,4 @@
import React from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react";
// hooks
@@ -102,72 +101,59 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
<button
type="button"
className={cn(
"group flex items-center gap-2 px-2 py-0.5 rounded outline-none",
"group flex items-center justify-between gap-2 px-2 py-0.5 rounded outline-none",
{
"cursor-not-allowed": disabled,
"hover:bg-custom-background-80": !disabled,
"bg-custom-background-80": isRelationModalOpen === relationKey,
},
className
)}
onClick={() => toggleRelationModal(relationKey)}
disabled={disabled}
>
<div className="flex items-start justify-between w-full">
{relationIssueIds.length > 0 ? (
<div className="flex items-center gap-2 py-0.5 flex-wrap">
{relationIssueIds.map((relationIssueId) => {
const currentIssue = issueMap[relationIssueId];
if (!currentIssue) return;
{relationIssueIds.length > 0 ? (
<div className="flex items-center gap-2 flex-wrap">
{relationIssueIds.map((relationIssueId) => {
const currentIssue = issueMap[relationIssueId];
if (!currentIssue) return;
const projectDetails = getProjectById(currentIssue.project_id);
const projectDetails = getProjectById(currentIssue.project_id);
return (
<div
key={relationIssueId}
className={`group flex items-center gap-1 rounded px-1.5 pt-1 pb-1 leading-3 hover:bg-custom-background-90 ${issueRelationObject[relationKey].className}`}
return (
<div
key={relationIssueId}
className={`group flex items-center gap-1 rounded px-1.5 py-1 ${issueRelationObject[relationKey].className}`}
>
<a
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium"
onClick={(e) => e.stopPropagation()}
>
<Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name}>
<Link
href={`/${workspaceSlug}/projects/${projectDetails?.id}/issues/${currentIssue.id}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium mt-0.5"
onClick={(e) => e.stopPropagation()}
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
</a>
{!disabled && (
<Tooltip tooltipContent="Remove">
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
removeRelation(workspaceSlug, projectId, issueId, relationKey, relationIssueId);
}}
>
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
</Link>
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
</span>
</Tooltip>
{!disabled && (
<Tooltip tooltipContent="Remove" position="bottom">
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
removeRelation(workspaceSlug, projectId, issueId, relationKey, relationIssueId);
}}
>
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
</span>
</Tooltip>
)}
</div>
);
})}
</div>
) : (
<span className="text-sm text-custom-text-400">{issueRelationObject[relationKey].placeholder}</span>
)}
{!disabled && (
<span
className={cn("p-1 flex-shrink-0 opacity-0 group-hover:opacity-100", {
"text-custom-text-400": relationIssueIds.length === 0,
})}
>
<Pencil className="h-2.5 w-2.5 flex-shrink-0" />
</span>
)}
</div>
)}
</div>
);
})}
</div>
) : (
<span className="text-sm text-custom-text-400">{issueRelationObject[relationKey].placeholder}</span>
)}
{!disabled && <Pencil className="h-4 w-4 flex-shrink-0 hidden group-hover:inline" />}
</button>
</>
);
+9 -11
View File
@@ -184,7 +184,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId?.toString() ?? ""}
placeholder="Add assignees"
multiple
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${
@@ -233,7 +233,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon
/>
</div>
@@ -258,7 +257,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon
/>
</div>
@@ -334,8 +332,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
/>
</div>
<div className="flex gap-2 min-h-8">
<div className="flex gap-1 pt-2 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<div className="flex items-center gap-2 min-h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<RelatedIcon className="h-4 w-4 flex-shrink-0" />
<span>Relates to</span>
</div>
@@ -349,8 +347,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
/>
</div>
<div className="flex gap-2 min-h-8">
<div className="flex gap-1 pt-2 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<div className="flex items-center gap-2 min-h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<XCircle className="h-4 w-4 flex-shrink-0" />
<span>Blocking</span>
</div>
@@ -364,8 +362,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
/>
</div>
<div className="flex gap-2 min-h-8">
<div className="flex gap-1 pt-2 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<div className="flex items-center gap-2 min-h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<CircleDot className="h-4 w-4 flex-shrink-0" />
<span>Blocked by</span>
</div>
@@ -379,8 +377,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
/>
</div>
<div className="flex gap-2 min-h-8">
<div className="flex gap-1 pt-2 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<div className="flex items-center gap-2 min-h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<CopyPlus className="h-4 w-4 flex-shrink-0" />
<span>Duplicate of</span>
</div>
@@ -1,7 +1,6 @@
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import size from "lodash/size";
import { useTheme } from "next-themes";
// hooks
import { useIssues, useUser } from "hooks/store";
// components
@@ -27,9 +26,6 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
membership: { currentProjectRole },
currentUser,
@@ -39,9 +35,12 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", isLightMode);
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath(
"empty-filters",
activeLayout ?? "list",
currentUser?.theme.theme === "light"
);
const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", currentUser?.theme.theme === "light");
const issueFilterCount = size(
Object.fromEntries(
@@ -1,7 +1,6 @@
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import size from "lodash/size";
import { useTheme } from "next-themes";
// hooks
import { useIssues, useUser } from "hooks/store";
// components
@@ -27,9 +26,6 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
membership: { currentProjectRole },
currentUser,
@@ -39,9 +35,12 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", isLightMode);
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath(
"empty-filters",
activeLayout ?? "list",
currentUser?.theme.theme === "light"
);
const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", currentUser?.theme.theme === "light");
const issueFilterCount = size(
Object.fromEntries(
@@ -1,7 +1,6 @@
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import size from "lodash/size";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useIssues, useUser } from "hooks/store";
// components
@@ -27,8 +26,6 @@ export const ProjectEmptyState: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
commandPalette: commandPaletteStore,
@@ -43,9 +40,12 @@ export const ProjectEmptyState: React.FC = observer(() => {
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", isLightMode);
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath(
"empty-filters",
activeLayout ?? "list",
currentUser?.theme.theme === "light"
);
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", currentUser?.theme.theme === "light");
const issueFilterCount = size(
Object.fromEntries(
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
// components
import { FilterHeader, FilterOption } from "components/issues";
// ui
@@ -18,7 +18,7 @@ type Props = {
searchQuery: string;
};
export const FilterLabels: React.FC<Props> = observer((props) => {
export const FilterLabels: React.FC<Props> = (props) => {
const { appliedFilters, handleUpdate, labels, searchQuery } = props;
const [itemsToRender, setItemsToRender] = useState(5);
@@ -80,4 +80,4 @@ export const FilterLabels: React.FC<Props> = observer((props) => {
)}
</>
);
});
};
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
// components
import { FilterHeader, FilterOption } from "components/issues";
// hooks
@@ -15,7 +15,7 @@ type Props = {
searchQuery: string;
};
export const FilterProjects: React.FC<Props> = observer((props) => {
export const FilterProjects: React.FC<Props> = (props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
// states
const [itemsToRender, setItemsToRender] = useState(5);
@@ -93,4 +93,4 @@ export const FilterProjects: React.FC<Props> = observer((props) => {
)}
</>
);
});
};
@@ -1,5 +1,5 @@
import { memo } from "react";
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
// components
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
@@ -16,11 +16,11 @@ interface IssueBlockProps {
issuesMap: IIssueMap;
displayProperties: IIssueDisplayProperties | undefined;
isDragDisabled: boolean;
draggableId: string;
index: number;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
}
interface IssueDetailsBlockProps {
@@ -90,11 +90,11 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
issuesMap,
displayProperties,
isDragDisabled,
draggableId,
index,
handleIssues,
quickActions,
canEditProperties,
provided,
snapshot,
} = props;
const issue = issuesMap[issueId];
@@ -104,38 +104,29 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
const canEditIssueProperties = canEditProperties(issue.project_id);
return (
<Draggable
key={draggableId}
draggableId={draggableId}
index={index}
isDragDisabled={!canEditIssueProperties || isDragDisabled}
<div
className="group/kanban-block relative p-1.5 hover:cursor-default"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
<div
className="group/kanban-block relative p-1.5 hover:cursor-default"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{issue.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<div
className={`space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
>
<KanbanIssueDetailsBlock
issue={issue}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
</div>
</div>
{issue.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
</Draggable>
<div
className={`space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
>
<KanbanIssueDetailsBlock
issue={issue}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
</div>
</div>
);
});
@@ -4,6 +4,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
// components
import { KanbanIssueBlock } from "components/issues";
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
interface IssueBlocksListProps {
sub_group_id: string;
@@ -42,18 +43,22 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
if (sub_group_id) draggableId = `${draggableId}__${sub_group_id}`;
return (
<KanbanIssueBlock
key={draggableId}
issueId={issueId}
issuesMap={issuesMap}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
draggableId={draggableId}
index={index}
isDragDisabled={isDragDisabled}
canEditProperties={canEditProperties}
/>
<Draggable key={draggableId} draggableId={draggableId} index={index} isDragDisabled={isDragDisabled}>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
<KanbanIssueBlock
key={`kanban-issue-block-${issueId}`}
issueId={issueId}
issuesMap={issuesMap}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
provided={provided}
snapshot={snapshot}
isDragDisabled={isDragDisabled}
canEditProperties={canEditProperties}
/>
)}
</Draggable>
);
})}
</>
@@ -28,7 +28,7 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => {
[EIssueActions.UPDATE]: async (issue: TIssue) => {
if (!workspaceSlug || !userId) return;
await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId);
await issues.updateIssue(workspaceSlug, userId, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug || !userId) return;
@@ -29,7 +29,7 @@ export const ProfileIssuesListLayout: FC = observer(() => {
[EIssueActions.UPDATE]: async (issue: TIssue) => {
if (!workspaceSlug || !userId) return;
await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId);
await issues.updateIssue(workspaceSlug, userId, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug || !userId) return;
@@ -82,17 +82,17 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels;
const options = projectLabels.map((label) => ({
value: label?.id,
query: label?.name,
value: label.id,
query: label.name,
content: (
<div className="flex items-center justify-start gap-2 overflow-hidden">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color,
backgroundColor: label.color,
}}
/>
<div className="line-clamp-1 inline-block truncate">{label?.name}</div>
<div className="line-clamp-1 inline-block truncate">{label.name}</div>
</div>
),
}));
@@ -106,11 +106,11 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
value.length <= maxRender ? (
<>
{projectLabels
?.filter((l) => value.includes(l?.id))
?.filter((l) => value.includes(l.id))
.map((label) => (
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={label?.name ?? ""}>
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={label.name ?? ""}>
<div
key={label?.id}
key={label.id}
className={`flex overflow-hidden hover:bg-custom-background-80 ${
!disabled && "cursor-pointer"
} h-full max-w-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs`}
@@ -122,7 +122,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
backgroundColor: label?.color ?? "#000000",
}}
/>
<div className="line-clamp-1 inline-block w-auto max-w-[100px] truncate">{label?.name}</div>
<div className="line-clamp-1 inline-block w-auto max-w-[100px] truncate">{label.name}</div>
</div>
</div>
</Tooltip>
@@ -138,8 +138,8 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
position="top"
tooltipHeading="Labels"
tooltipContent={projectLabels
?.filter((l) => value.includes(l?.id))
.map((l) => l?.name)
?.filter((l) => value.includes(l.id))
.map((l) => l.name)
.join(", ")}
>
<div className="flex h-full items-center gap-1.5 text-custom-text-200">
@@ -3,7 +3,6 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import isEmpty from "lodash/isEmpty";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useGlobalView, useIssues, useProject, useUser } from "hooks/store";
import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties";
@@ -26,8 +25,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, globalViewId } = router.query;
// theme
const { resolvedTheme } = useTheme();
//swr hook for fetching issue properties
useWorkspaceIssueProperties(workspaceSlug);
// store
@@ -49,8 +46,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view";
const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode);
const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, currentUser?.theme.theme === "light");
// filter init from the query params
+4 -4
View File
@@ -277,8 +277,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
changesMade={changesMade}
data={{
...data,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
module_id: data?.module_id ? data?.module_id : moduleId ? moduleId : null,
cycle_id: cycleId ?? null,
module_id: moduleId ?? null,
}}
onChange={handleFormChange}
onClose={handleClose}
@@ -291,8 +291,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
<IssueFormRoot
data={{
...data,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
module_id: data?.module_id ? data?.module_id : moduleId ? moduleId : null,
cycle_id: cycleId ?? null,
module_id: moduleId ?? null,
}}
onClose={() => handleClose(false)}
isCreateMoreToggleEnabled={createMore}
@@ -99,7 +99,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
projectId={projectId}
placeholder="Add assignees"
multiple
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
className="w-3/4 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"}`}
@@ -148,7 +148,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon
/>
</div>
@@ -174,7 +173,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
showPlaceholderIcon
/>
</div>
@@ -253,8 +251,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
</div>
{/* relates to */}
<div className="flex gap-3 min-h-8">
<div className="flex pt-2 gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<div className="flex items-center gap-3 min-h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<RelatedIcon className="h-4 w-4 flex-shrink-0" />
<span>Relates to</span>
</div>
@@ -269,8 +267,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
</div>
{/* blocking */}
<div className="flex gap-3 min-h-8">
<div className="flex pt-2 gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<div className="flex items-center gap-3 min-h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<XCircle className="h-4 w-4 flex-shrink-0" />
<span>Blocking</span>
</div>
@@ -285,8 +283,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
</div>
{/* blocked by */}
<div className="flex gap-3 min-h-8">
<div className="flex pt-2 gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<div className="flex items-center gap-3 min-h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CircleDot className="h-4 w-4 flex-shrink-0" />
<span>Blocked by</span>
</div>
@@ -301,8 +299,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
</div>
{/* duplicate of */}
<div className="flex gap-3 min-h-8">
<div className="flex pt-2 gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<div className="flex items-center gap-3 min-h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CopyPlus className="h-4 w-4 flex-shrink-0" />
<span>Duplicate of</span>
</div>
+1 -5
View File
@@ -1,6 +1,5 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useModule, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
@@ -16,8 +15,6 @@ export const ModulesListView: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId, peekModule } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const {
@@ -28,8 +25,7 @@ export const ModulesListView: React.FC = observer(() => {
const { storedValue: modulesView } = useLocalStorage("modules_view", "grid");
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", currentUser?.theme.theme === "light");
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
+92 -113
View File
@@ -1,8 +1,20 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
// hooks
import { useModule, useUser } from "hooks/store";
// hooks
import useToast from "hooks/use-toast";
// components
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
import { DeleteModuleModal } from "components/modules";
import ProgressChart from "components/core/sidebar/progress-chart";
// ui
import { CustomRangeDatePicker } from "components/ui";
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
// icon
import {
AlertCircle,
CalendarCheck2,
@@ -15,17 +27,6 @@ import {
Trash2,
UserCircle2,
} from "lucide-react";
// hooks
import { useModule, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
import { DeleteModuleModal } from "components/modules";
import ProgressChart from "components/core/sidebar/progress-chart";
import { ProjectMemberDropdown } from "components/dropdowns";
// ui
import { CustomRangeDatePicker } from "components/ui";
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
// helpers
import { isDateGreaterThanToday, renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
@@ -34,6 +35,7 @@ import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
// constant
import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project";
import { ProjectMemberDropdown } from "components/dropdowns";
const defaultValues: Partial<IModule> = {
lead: "",
@@ -55,9 +57,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
// refs
const startDateButtonRef = useRef<HTMLButtonElement | null>(null);
const endDateButtonRef = useRef<HTMLButtonElement | null>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId, peekModule } = router.query;
@@ -165,8 +164,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const handleStartDateChange = async (date: string) => {
setValue("start_date", date);
if (!watch("target_date") || watch("target_date") === "") endDateButtonRef.current?.click();
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
if (!isDateGreaterThanToday(`${watch("target_date")}`)) {
setToastAlert({
@@ -174,9 +171,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
title: "Error!",
message: "Unable to create module in past date. Please enter a valid date.",
});
reset({
...moduleDetails,
});
return;
}
@@ -195,8 +189,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const handleEndDateChange = async (date: string) => {
setValue("target_date", date);
if (!watch("start_date") || watch("start_date") === "") endDateButtonRef.current?.click();
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
if (!isDateGreaterThanToday(`${watch("target_date")}`)) {
setToastAlert({
@@ -204,9 +196,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
title: "Error!",
message: "Unable to create module in past date. Please enter a valid date.",
});
reset({
...moduleDetails,
});
return;
}
@@ -366,55 +355,49 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<CalendarClock className="h-4 w-4" />
<span className="text-base">Start date</span>
<span className="text-base">Start Date</span>
</div>
<div className="relative flex w-1/2 items-center rounded-sm">
<Popover className="flex h-full w-full items-center justify-center rounded-lg">
{({ close }) => (
<>
<Popover.Button
ref={startDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("start_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(startDate) ?? "No date selected"}
</span>
</Popover.Button>
<Popover.Button
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("start_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(startDate) ?? "No date selected"}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : moduleDetails?.start_date}
onChange={(val) => {
if (val) {
handleStartDateChange(val);
close();
}
}}
startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("target_date")}`)}
selectsStart={watch("target_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
)}
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : moduleDetails?.start_date}
onChange={(val) => {
if (val) {
handleStartDateChange(val);
}
}}
startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("target_date")}`)}
selectsStart={watch("target_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</Popover>
</div>
</div>
@@ -422,55 +405,51 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<CalendarCheck2 className="h-4 w-4" />
<span className="text-base">Target date</span>
<span className="text-base">Target Date</span>
</div>
<div className="relative flex w-1/2 items-center rounded-sm">
<Popover className="flex h-full w-full items-center justify-center rounded-lg">
{({ close }) => (
<>
<Popover.Button
ref={endDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
<>
<Popover.Button
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("target_date") ? "" : "text-custom-text-400"
}`}
disabled={!isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("target_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(endDate) ?? "No date selected"}
</span>
</Popover.Button>
{renderFormattedDate(endDate) ?? "No date selected"}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("target_date") ? watch("target_date") : moduleDetails?.target_date}
onChange={(val) => {
if (val) {
handleEndDateChange(val);
close();
}
}}
startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
)}
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("target_date") ? watch("target_date") : moduleDetails?.target_date}
onChange={(val) => {
if (val) {
handleEndDateChange(val);
}
}}
startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
</Popover>
</div>
</div>
@@ -95,7 +95,6 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
<MoreVertical className="h-3.5 w-3.5" />
</div>
}
closeOnSelect
>
<CustomMenu.MenuItem onClick={markAllNotificationsAsRead}>
<div className="flex items-center gap-2">
@@ -1,5 +1,4 @@
import { useEffect } from "react";
import { useTheme } from "next-themes";
import { observer } from "mobx-react-lite";
// hooks
import { useApplication, useDashboard, useProject, useUser } from "hooks/store";
@@ -15,8 +14,6 @@ import { Spinner } from "@plane/ui";
import { EUserWorkspaceRoles } from "constants/workspace";
export const WorkspaceDashboardView = observer(() => {
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
commandPalette: { toggleCreateProjectModal },
@@ -31,8 +28,7 @@ export const WorkspaceDashboardView = observer(() => {
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
const { joinedProjectIds } = useProject();
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode);
const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", currentUser?.theme.theme === "light");
const handleTourCompleted = () => {
updateTourCompleted()
@@ -1,6 +1,5 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
@@ -19,9 +18,7 @@ type IPagesListView = {
export const PagesListView: FC<IPagesListView> = (props) => {
const { pageIds: projectPageIds } = props;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
commandPalette: { toggleCreatePageModal },
} = useApplication();
@@ -39,8 +36,11 @@ export const PagesListView: FC<IPagesListView> = (props) => {
? PAGE_EMPTY_STATE_DETAILS[pageTab as keyof typeof PAGE_EMPTY_STATE_DETAILS]
: PAGE_EMPTY_STATE_DETAILS["All"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("pages", currentPageTabDetails.key, isLightMode);
const emptyStateImage = getEmptyStateImagePath(
"pages",
currentPageTabDetails.key,
currentUser?.theme.theme === "light"
);
const isButtonVisible = currentPageTabDetails.key !== "archived" && currentPageTabDetails.key !== "favorites";
@@ -1,6 +1,5 @@
import React, { FC } from "react";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useUser } from "hooks/store";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
@@ -15,8 +14,6 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { EUserProjectRoles } from "constants/project";
export const RecentPagesList: FC = observer(() => {
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const {
@@ -25,8 +22,7 @@ export const RecentPagesList: FC = observer(() => {
} = useUser();
const { recentProjectPages } = useProjectPages();
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", currentUser?.theme.theme === "light");
// FIXME: replace any with proper type
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
+2 -2
View File
@@ -1,5 +1,5 @@
export * from "./overview";
export * from "./navbar";
export * from "./profile-issues-filter";
export * from "./sidebar";
export * from "./time";
export * from "./profile-issues-filter";
+1 -6
View File
@@ -2,7 +2,6 @@ import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// components
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root";
@@ -28,9 +27,6 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
workspaceSlug: string;
userId: string;
};
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
membership: { currentWorkspaceRole },
currentUser,
@@ -50,8 +46,7 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
}
);
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("profile", type, isLightMode);
const emptyStateImage = getEmptyStateImagePath("profile", type, currentUser?.theme.theme === "light");
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
+15 -3
View File
@@ -7,8 +7,6 @@ import { observer } from "mobx-react-lite";
import { useUser } from "hooks/store";
// services
import { UserService } from "services/user.service";
// components
import { ProfileSidebarTime } from "./time";
// ui
import { Loader, Tooltip } from "@plane/ui";
// icons
@@ -36,6 +34,16 @@ export const ProfileSidebar = observer(() => {
: null
);
// Create a date object for the current time in the specified timezone
const currentTime = new Date();
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: userProjectsData?.user_data.user_timezone,
hour12: false, // Use 24-hour format
hour: "2-digit",
minute: "2-digit",
});
const timeString = formatter.format(currentTime);
const userDetails = [
{
label: "Joined on",
@@ -43,7 +51,11 @@ export const ProfileSidebar = observer(() => {
},
{
label: "Timezone",
value: <ProfileSidebarTime timeZone={userProjectsData?.user_data.user_timezone} />,
value: (
<span>
{timeString} <span className="text-custom-text-200">{userProjectsData?.user_data.user_timezone}</span>
</span>
),
},
];
-27
View File
@@ -1,27 +0,0 @@
// hooks
import { useCurrentTime } from "hooks/use-current-time";
type Props = {
timeZone: string | undefined;
};
export const ProfileSidebarTime: React.FC<Props> = (props) => {
const { timeZone } = props;
// current time hook
const { currentTime } = useCurrentTime();
// Create a date object for the current time in the specified timezone
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timeZone,
hour12: false, // Use 24-hour format
hour: "2-digit",
minute: "2-digit",
});
const timeString = formatter.format(currentTime);
return (
<span>
{timeString} <span className="text-custom-text-200">{timeZone}</span>
</span>
);
};
+3 -5
View File
@@ -1,17 +1,16 @@
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useProject, useUser } from "hooks/store";
// components
import { ProjectCard } from "components/project";
import { Loader } from "@plane/ui";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// icons
import { Plus } from "lucide-react";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectCardList = observer(() => {
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
commandPalette: commandPaletteStore,
@@ -23,8 +22,7 @@ export const ProjectCardList = observer(() => {
} = useUser();
const { workspaceProjectIds, searchedProjects, getProjectById } = useProject();
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", isLightMode);
const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", currentUser?.theme.theme === "light");
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
+1 -1
View File
@@ -206,7 +206,7 @@ export const ProjectSidebarList: FC = observer(() => {
type="button"
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
>
Your projects
Projects
{open ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
+2 -5
View File
@@ -1,7 +1,4 @@
import { FC } from "react";
// hooks
import { useCurrentTime } from "hooks/use-current-time";
// types
import { IUser } from "@plane/types";
export interface IUserGreetingsView {
@@ -10,8 +7,8 @@ export interface IUserGreetingsView {
export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
const { user } = props;
// current time hook
const { currentTime } = useCurrentTime();
const currentTime = new Date();
const hour = new Intl.DateTimeFormat("en-US", {
hour12: false,
+1 -5
View File
@@ -1,7 +1,6 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { Search } from "lucide-react";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useProjectView, useUser } from "hooks/store";
// components
@@ -15,8 +14,6 @@ import { EUserProjectRoles } from "constants/project";
export const ProjectViewsList = observer(() => {
// states
const [query, setQuery] = useState("");
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
commandPalette: { toggleCreateViewModal },
@@ -46,8 +43,7 @@ export const ProjectViewsList = observer(() => {
const viewsList = projectViewIds.map((viewId) => getViewById(viewId));
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", currentUser?.theme.theme === "light");
const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase()));
+1 -11
View File
@@ -304,17 +304,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
},
my_issues: {
spreadsheet: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
filters: ["priority", "state_group", "labels", "assignees", "created_by", "project", "start_date", "target_date"],
display_properties: true,
display_filters: {
type: [null, "active", "backlog"],
-17
View File
@@ -1,17 +0,0 @@
import { useEffect, useState } from "react";
export const useCurrentTime = () => {
const [currentTime, setCurrentTime] = useState(new Date());
// update the current time every second
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(intervalId);
}, []);
return {
currentTime,
};
};
-7
View File
@@ -264,13 +264,6 @@ const useUserNotification = () => {
await userNotificationServices
.markAllNotificationsAsRead(workspaceSlug.toString(), markAsReadParams)
.then(() => {
setToastAlert({
type: "success",
title: "Success!",
message: "All Notifications marked as read.",
});
})
.catch(() => {
setToastAlert({
type: "error",
+1 -5
View File
@@ -1,7 +1,6 @@
import React, { Fragment, ReactElement } from "react";
import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useProject, useUser } from "hooks/store";
// layouts
@@ -17,8 +16,6 @@ import { EUserWorkspaceRoles } from "constants/workspace";
import { NextPageWithLayout } from "lib/types";
const AnalyticsPage: NextPageWithLayout = observer(() => {
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
commandPalette: { toggleCreateProjectModal },
@@ -30,8 +27,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
} = useUser();
const { workspaceProjectIds } = useProject();
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", currentUser?.theme.theme === "light");
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
@@ -2,7 +2,6 @@ import { Fragment, useCallback, useState, ReactElement } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react";
import { useTheme } from "next-themes";
// hooks
import { useCycle, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
@@ -23,8 +22,6 @@ import { EUserWorkspaceRoles } from "constants/workspace";
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const [createModal, setCreateModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
membership: { currentProjectRole },
@@ -52,9 +49,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
},
[handleCurrentLayout, setCycleTab]
);
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", currentUser?.theme.theme === "light");
const totalCycles = currentProjectCycleIds?.length ?? 0;
@@ -4,7 +4,6 @@ import dynamic from "next/dynamic";
import { Tab } from "@headlessui/react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useApplication, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
@@ -49,9 +48,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
const { workspaceSlug, projectId } = router.query;
// states
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks
// store
const {
currentUser,
currentUserLoader,
@@ -97,8 +94,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
}
};
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode);
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", currentUser?.theme.theme === "light");
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@@ -31,10 +31,7 @@ export interface IIssueFilterHelperStore {
filteredParams: TIssueParams[]
): Partial<Record<TIssueParams, string | boolean>>;
computedFilters(filters: IIssueFilterOptions): IIssueFilterOptions;
computedDisplayFilters(
displayFilters: IIssueDisplayFilterOptions,
defaultValues?: IIssueDisplayFilterOptions
): IIssueDisplayFilterOptions;
computedDisplayFilters(displayFilters: IIssueDisplayFilterOptions): IIssueDisplayFilterOptions;
computedDisplayProperties(filters: IIssueDisplayProperties): IIssueDisplayProperties;
}
@@ -76,8 +73,6 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
labels: filters?.labels || undefined,
start_date: filters?.start_date || undefined,
target_date: filters?.target_date || undefined,
project: filters.project || undefined,
subscriber: filters.subscriber || undefined,
// display filters
type: displayFilters?.type || undefined,
sub_issue: displayFilters?.sub_issue ?? true,
@@ -152,27 +147,20 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
* @param {IIssueDisplayFilterOptions} displayFilters
* @returns {IIssueDisplayFilterOptions}
*/
computedDisplayFilters = (
displayFilters: IIssueDisplayFilterOptions,
defaultValues?: IIssueDisplayFilterOptions
): IIssueDisplayFilterOptions => {
const filters = displayFilters || defaultValues;
return {
calendar: {
show_weekends: filters?.calendar?.show_weekends || false,
layout: filters?.calendar?.layout || "month",
},
layout: filters?.layout || "list",
order_by: filters?.order_by || "sort_order",
group_by: filters?.group_by || null,
sub_group_by: filters?.sub_group_by || null,
type: filters?.type || null,
sub_issue: filters?.sub_issue || false,
show_empty_groups: filters?.show_empty_groups || false,
start_target_date: filters?.start_target_date || false,
};
};
computedDisplayFilters = (displayFilters: IIssueDisplayFilterOptions): IIssueDisplayFilterOptions => ({
calendar: {
show_weekends: displayFilters?.calendar?.show_weekends || false,
layout: displayFilters?.calendar?.layout || "month",
},
layout: displayFilters?.layout || "list",
order_by: displayFilters?.order_by || "sort_order",
group_by: displayFilters?.group_by || null,
sub_group_by: displayFilters?.sub_group_by || null,
type: displayFilters?.type || null,
sub_issue: displayFilters?.sub_issue || false,
show_empty_groups: displayFilters?.show_empty_groups || false,
start_target_date: displayFilters?.start_target_date || false,
});
/**
* @description This method is used to apply the display properties on the issues
+2 -2
View File
@@ -90,7 +90,7 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
const userFilters = this.getIssueFilters(viewId);
if (!userFilters) return undefined;
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "my_issues");
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
if (!filteredParams) return undefined;
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
@@ -126,7 +126,7 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
};
const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.GLOBAL, workspaceSlug, undefined, viewId);
displayFilters = this.computedDisplayFilters(_filters?.display_filters, { layout: "spreadsheet" });
displayFilters = this.computedDisplayFilters(_filters?.display_filters);
displayProperties = this.computedDisplayProperties(_filters?.display_properties);
kanbanFilters = {
group_by: _filters?.kanban_filters?.group_by || [],