Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 960bf03ac1 | |||
| c7616fda11 | |||
| 483fc57601 | |||
| 09a1a55da8 | |||
| 61f92563a9 | |||
| 00e07443b0 | |||
| c4efdcd704 | |||
| 3c9679dff9 | |||
| b3393f5c48 | |||
| f995736642 | |||
| f7f1f2bea4 |
@@ -304,6 +304,7 @@ 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:
|
||||
@@ -313,6 +314,7 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
@@ -328,6 +330,7 @@ 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:
|
||||
@@ -337,6 +340,7 @@ class RelatedIssueSerializer(BaseSerializer):
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
|
||||
@@ -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.5
|
||||
cryptography==41.0.6
|
||||
lxml==4.9.3
|
||||
boto3==1.28.40
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
);
|
||||
},
|
||||
}}
|
||||
margin={{ top: 20 }}
|
||||
margin={{ top: 20, right: 20 }}
|
||||
theme={{
|
||||
axis: {},
|
||||
}}
|
||||
|
||||
@@ -6,54 +6,76 @@ 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 }) => (
|
||||
<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}
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
@@ -43,6 +44,7 @@ interface IActiveCycleDetails {
|
||||
export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, projectId } = props;
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const {
|
||||
@@ -76,7 +78,9 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
);
|
||||
|
||||
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"];
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle", "active", currentUser?.theme.theme === "light");
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle", "active", isLightMode);
|
||||
|
||||
if (!activeCycle && isLoading)
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
// components
|
||||
@@ -18,11 +19,15 @@ 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 emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light");
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
// components
|
||||
@@ -19,11 +20,15 @@ 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 emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light");
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -46,6 +46,11 @@ type Props = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<ICycle> = {
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
};
|
||||
|
||||
// services
|
||||
const cycleService = new CycleService();
|
||||
|
||||
@@ -54,6 +59,9 @@ 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;
|
||||
@@ -70,11 +78,6 @@ 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,
|
||||
});
|
||||
@@ -120,6 +123,9 @@ 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({
|
||||
@@ -127,6 +133,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -147,7 +154,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
title: "Success!",
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@@ -155,8 +161,10 @@ 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({
|
||||
@@ -181,6 +189,7 @@ 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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -188,6 +197,8 @@ 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({
|
||||
@@ -195,6 +206,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -215,7 +227,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
title: "Success!",
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@@ -223,8 +234,9 @@ 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({
|
||||
@@ -249,6 +261,7 @@ 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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -387,50 +400,56 @@ 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">
|
||||
<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>
|
||||
{({ 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
@@ -438,52 +457,56 @@ 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">
|
||||
<>
|
||||
<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"
|
||||
{({ 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"
|
||||
}`}
|
||||
disabled={isCompleted || !isEditingAllowed}
|
||||
>
|
||||
{renderFormattedDate(endDate) ?? "No date selected"}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</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 className="lg:col-span-2">
|
||||
<div key={key} className="lg:col-span-2">
|
||||
<WidgetComponent dashboardId={homeDashboardId} workspaceSlug={workspaceSlug} />
|
||||
</div>
|
||||
);
|
||||
else return <WidgetComponent dashboardId={homeDashboardId} workspaceSlug={workspaceSlug} />;
|
||||
else return <WidgetComponent key={key} 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 as="div" className="h-full flex flex-col">
|
||||
<Tab.Panel key={tab.key} 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 src={member.member__avatar} name={member.member__display_name} />
|
||||
<Avatar key={member.member_id} src={member.member__avatar} name={member.member__display_name} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ type ButtonProps = {
|
||||
hideIcon: boolean;
|
||||
hideText?: boolean;
|
||||
dropdownArrow: boolean;
|
||||
isActive?: boolean;
|
||||
dropdownArrowClassName: string;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
@@ -51,6 +52,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
@@ -60,6 +62,7 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -111,6 +114,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
@@ -120,6 +124,7 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -268,6 +273,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
@@ -279,6 +285,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
@@ -310,6 +317,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
@@ -321,6 +329,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -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 { CalendarDays, X } from "lucide-react";
|
||||
import { Calendar, CalendarDays, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
@@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
||||
onChange: (val: Date | null) => void;
|
||||
value: Date | string | null;
|
||||
closeOnSelect?: boolean;
|
||||
showPlaceholderIcon?: boolean;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@@ -33,9 +34,11 @@ type ButtonProps = {
|
||||
isClearable: boolean;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
onClear: () => void;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
showPlaceholderIcon?: boolean;
|
||||
};
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
@@ -47,6 +50,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
isClearable,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
@@ -61,6 +65,7 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -131,9 +136,11 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
isClearable,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
showPlaceholderIcon = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -145,11 +152,16 @@ 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)}
|
||||
@@ -183,6 +195,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placement,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
showPlaceholderIcon = false,
|
||||
value,
|
||||
} = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -246,6 +259,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
@@ -258,6 +272,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
@@ -296,7 +311,9 @@ 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
|
||||
@@ -308,8 +325,10 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
showPlaceholderIcon={showPlaceholderIcon}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
@@ -31,6 +31,7 @@ type ButtonProps = {
|
||||
dropdownArrowClassName: string;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
};
|
||||
@@ -51,6 +52,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
@@ -64,6 +66,7 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -123,6 +126,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
@@ -136,6 +140,7 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -276,6 +281,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
@@ -287,6 +293,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
@@ -318,6 +325,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
@@ -329,6 +337,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -14,6 +14,7 @@ type ButtonProps = {
|
||||
placeholder: string;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip: boolean;
|
||||
userIds: string | string[] | null;
|
||||
};
|
||||
@@ -50,6 +51,7 @@ export const BorderButton = observer((props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
userIds,
|
||||
tooltip,
|
||||
@@ -57,7 +59,7 @@ export const BorderButton = observer((props: ButtonProps) => {
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const isMultiple = Array.isArray(userIds);
|
||||
const isArray = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
@@ -68,13 +70,18 @@ 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">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
@@ -99,7 +106,7 @@ export const BackgroundButton = observer((props: ButtonProps) => {
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const isMultiple = Array.isArray(userIds);
|
||||
const isArray = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
@@ -115,8 +122,12 @@ export const BackgroundButton = observer((props: ButtonProps) => {
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
@@ -134,6 +145,7 @@ export const TransparentButton = observer((props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
placeholder,
|
||||
userIds,
|
||||
tooltip,
|
||||
@@ -141,7 +153,7 @@ export const TransparentButton = observer((props: ButtonProps) => {
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
const isMultiple = Array.isArray(userIds);
|
||||
const isArray = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
@@ -152,13 +164,18 @@ 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">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
<span className="flex-grow truncate text-sm leading-5">
|
||||
{isArray && userIds.length > 0
|
||||
? userIds.length === 1
|
||||
? getUserDetails(userIds[0])?.display_name
|
||||
: ""
|
||||
: placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
|
||||
@@ -147,6 +147,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
@@ -157,6 +158,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
@@ -189,6 +191,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
@@ -199,6 +202,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
|
||||
@@ -38,6 +38,7 @@ type ButtonProps = {
|
||||
dropdownArrowClassName: string;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
module: IModule | null;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
@@ -50,6 +51,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
module,
|
||||
placeholder,
|
||||
tooltip,
|
||||
@@ -60,6 +62,7 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -110,6 +113,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
module,
|
||||
placeholder,
|
||||
tooltip,
|
||||
@@ -120,6 +124,7 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -267,6 +272,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
@@ -278,6 +284,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
@@ -309,6 +316,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
@@ -320,6 +328,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -31,6 +31,7 @@ type ButtonProps = {
|
||||
dropdownArrowClassName: string;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
highlightUrgent: boolean;
|
||||
priority: TIssuePriorities;
|
||||
tooltip: boolean;
|
||||
@@ -181,6 +182,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
highlightUrgent,
|
||||
priority,
|
||||
tooltip,
|
||||
@@ -207,6 +209,7 @@ 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
|
||||
)}
|
||||
@@ -312,7 +315,13 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
as="div"
|
||||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("h-full", className)}
|
||||
className={cn(
|
||||
"h-full",
|
||||
{
|
||||
"bg-custom-background-80": isOpen,
|
||||
},
|
||||
className
|
||||
)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
@@ -402,6 +411,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
@@ -414,6 +424,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,7 @@ type ButtonProps = {
|
||||
dropdownArrowClassName: string;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
isActive?: boolean;
|
||||
state: IState | undefined;
|
||||
tooltip: boolean;
|
||||
};
|
||||
@@ -41,6 +42,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
state,
|
||||
tooltip,
|
||||
} = props;
|
||||
@@ -50,6 +52,9 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -111,6 +116,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
isActive = false,
|
||||
state,
|
||||
tooltip,
|
||||
} = props;
|
||||
@@ -120,6 +126,9 @@ 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
|
||||
)}
|
||||
>
|
||||
@@ -251,6 +260,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
@@ -260,6 +270,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
@@ -289,6 +300,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
@@ -298,6 +310,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
isActive={isOpen}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
|
||||
@@ -64,6 +64,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
{
|
||||
"cursor-not-allowed": disabled,
|
||||
"hover:bg-custom-background-80": !disabled,
|
||||
"bg-custom-background-80": isParentIssueModalOpen,
|
||||
},
|
||||
className
|
||||
)}
|
||||
@@ -72,15 +73,20 @@ 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">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${parentIssue?.id}`}
|
||||
className="text-xs font-medium"
|
||||
>
|
||||
{parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id}
|
||||
</Link>
|
||||
<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>
|
||||
|
||||
{!disabled && (
|
||||
<Tooltip tooltipContent="Remove">
|
||||
<Tooltip tooltipContent="Remove" position="bottom">
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -96,7 +102,15 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
) : (
|
||||
<span className="text-sm text-custom-text-400">Add parent issue</span>
|
||||
)}
|
||||
{!disabled && <Pencil className="h-4 w-4 flex-shrink-0 hidden group-hover:inline" />}
|
||||
{!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>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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
|
||||
@@ -101,59 +102,72 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"group flex items-center justify-between gap-2 px-2 py-0.5 rounded outline-none",
|
||||
"group flex items-center 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}
|
||||
>
|
||||
{relationIssueIds.length > 0 ? (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{relationIssueIds.map((relationIssueId) => {
|
||||
const currentIssue = issueMap[relationIssueId];
|
||||
if (!currentIssue) return;
|
||||
<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;
|
||||
|
||||
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 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()}
|
||||
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}`}
|
||||
>
|
||||
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
|
||||
</a>
|
||||
{!disabled && (
|
||||
<Tooltip tooltipContent="Remove">
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
removeRelation(workspaceSlug, projectId, issueId, relationKey, relationIssueId);
|
||||
}}
|
||||
<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()}
|
||||
>
|
||||
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
|
||||
</span>
|
||||
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
</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" />}
|
||||
{!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>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -184,7 +184,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
placeholder="Add assignees"
|
||||
multiple
|
||||
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
|
||||
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm justify-between ${
|
||||
@@ -233,6 +233,7 @@ 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>
|
||||
|
||||
@@ -257,6 +258,7 @@ 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>
|
||||
|
||||
@@ -332,8 +334,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<RelatedIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Relates to</span>
|
||||
</div>
|
||||
@@ -347,8 +349,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<XCircle className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Blocking</span>
|
||||
</div>
|
||||
@@ -362,8 +364,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<CircleDot className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Blocked by</span>
|
||||
</div>
|
||||
@@ -377,8 +379,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<CopyPlus className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Duplicate of</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -26,6 +27,9 @@ 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,
|
||||
@@ -35,12 +39,9 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath(
|
||||
"empty-filters",
|
||||
activeLayout ?? "list",
|
||||
currentUser?.theme.theme === "light"
|
||||
);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", currentUser?.theme.theme === "light");
|
||||
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 issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -26,6 +27,9 @@ 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,
|
||||
@@ -35,12 +39,9 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath(
|
||||
"empty-filters",
|
||||
activeLayout ?? "list",
|
||||
currentUser?.theme.theme === "light"
|
||||
);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", currentUser?.theme.theme === "light");
|
||||
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 issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -26,6 +27,8 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
commandPalette: commandPaletteStore,
|
||||
@@ -40,12 +43,9 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath(
|
||||
"empty-filters",
|
||||
activeLayout ?? "list",
|
||||
currentUser?.theme.theme === "light"
|
||||
);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", isLightMode);
|
||||
|
||||
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> = (props) => {
|
||||
export const FilterLabels: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, labels, searchQuery } = props;
|
||||
|
||||
const [itemsToRender, setItemsToRender] = useState(5);
|
||||
@@ -80,4 +80,4 @@ export const FilterLabels: React.FC<Props> = (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> = (props) => {
|
||||
export const FilterProjects: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||
// states
|
||||
const [itemsToRender, setItemsToRender] = useState(5);
|
||||
@@ -93,4 +93,4 @@ export const FilterProjects: React.FC<Props> = (props) => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo } from "react";
|
||||
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
||||
import { Draggable, 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,29 +104,38 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
||||
const canEditIssueProperties = canEditProperties(issue.project_id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group/kanban-block relative p-1.5 hover:cursor-default"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
<Draggable
|
||||
key={draggableId}
|
||||
draggableId={draggableId}
|
||||
index={index}
|
||||
isDragDisabled={!canEditIssueProperties || isDragDisabled}
|
||||
>
|
||||
{issue.tempId !== undefined && (
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
{(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>
|
||||
)}
|
||||
<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>
|
||||
</Draggable>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -43,22 +42,18 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
||||
if (sub_group_id) draggableId = `${draggableId}__${sub_group_id}`;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<KanbanIssueBlock
|
||||
key={draggableId}
|
||||
issueId={issueId}
|
||||
issuesMap={issuesMap}
|
||||
displayProperties={displayProperties}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
draggableId={draggableId}
|
||||
index={index}
|
||||
isDragDisabled={isDragDisabled}
|
||||
canEditProperties={canEditProperties}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => {
|
||||
[EIssueActions.UPDATE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
|
||||
await issues.updateIssue(workspaceSlug, userId, issue.id, issue);
|
||||
await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId);
|
||||
},
|
||||
[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, userId, issue.id, issue);
|
||||
await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId);
|
||||
},
|
||||
[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,6 +3,7 @@ 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";
|
||||
@@ -25,6 +26,8 @@ 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
|
||||
@@ -46,7 +49,8 @@ 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 emptyStateImage = getEmptyStateImagePath("all-issues", currentView, currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode);
|
||||
|
||||
// filter init from the query params
|
||||
|
||||
|
||||
@@ -277,8 +277,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
changesMade={changesMade}
|
||||
data={{
|
||||
...data,
|
||||
cycle_id: cycleId ?? null,
|
||||
module_id: moduleId ?? null,
|
||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||
module_id: data?.module_id ? data?.module_id : moduleId ? moduleId : null,
|
||||
}}
|
||||
onChange={handleFormChange}
|
||||
onClose={handleClose}
|
||||
@@ -291,8 +291,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
<IssueFormRoot
|
||||
data={{
|
||||
...data,
|
||||
cycle_id: cycleId ?? null,
|
||||
module_id: moduleId ?? null,
|
||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||
module_id: data?.module_id ? data?.module_id : moduleId ? 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 > 0 ? "transparent-without-text" : "transparent-with-text"}
|
||||
buttonVariant={issue?.assignee_ids?.length > 1 ? "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,6 +148,7 @@ 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>
|
||||
|
||||
@@ -173,6 +174,7 @@ 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>
|
||||
|
||||
@@ -251,8 +253,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
</div>
|
||||
|
||||
{/* relates to */}
|
||||
<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">
|
||||
<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">
|
||||
<RelatedIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Relates to</span>
|
||||
</div>
|
||||
@@ -267,8 +269,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
</div>
|
||||
|
||||
{/* blocking */}
|
||||
<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">
|
||||
<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">
|
||||
<XCircle className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Blocking</span>
|
||||
</div>
|
||||
@@ -283,8 +285,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
</div>
|
||||
|
||||
{/* blocked by */}
|
||||
<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">
|
||||
<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">
|
||||
<CircleDot className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Blocked by</span>
|
||||
</div>
|
||||
@@ -299,8 +301,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
</div>
|
||||
|
||||
{/* duplicate of */}
|
||||
<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">
|
||||
<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">
|
||||
<CopyPlus className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Duplicate of</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -15,6 +16,8 @@ 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 {
|
||||
@@ -25,7 +28,8 @@ export const ModulesListView: React.FC = observer(() => {
|
||||
|
||||
const { storedValue: modulesView } = useLocalStorage("modules_view", "grid");
|
||||
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, 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,
|
||||
@@ -27,6 +15,17 @@ 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";
|
||||
@@ -35,7 +34,6 @@ 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: "",
|
||||
@@ -57,6 +55,9 @@ 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;
|
||||
@@ -164,6 +165,8 @@ 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({
|
||||
@@ -171,6 +174,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -189,6 +195,8 @@ 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({
|
||||
@@ -196,6 +204,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -355,49 +366,55 @@ 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">
|
||||
<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>
|
||||
{({ 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
@@ -405,51 +422,55 @@ 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">
|
||||
<>
|
||||
<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"
|
||||
{({ 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"
|
||||
}`}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
{renderFormattedDate(endDate) ?? "No date selected"}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,6 +95,7 @@ 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,4 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useApplication, useDashboard, useProject, useUser } from "hooks/store";
|
||||
@@ -14,6 +15,8 @@ import { Spinner } from "@plane/ui";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const WorkspaceDashboardView = observer(() => {
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
commandPalette: { toggleCreateProjectModal },
|
||||
@@ -28,7 +31,8 @@ export const WorkspaceDashboardView = observer(() => {
|
||||
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
|
||||
const { joinedProjectIds } = useProject();
|
||||
|
||||
const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode);
|
||||
|
||||
const handleTourCompleted = () => {
|
||||
updateTourCompleted()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -18,7 +19,9 @@ type IPagesListView = {
|
||||
|
||||
export const PagesListView: FC<IPagesListView> = (props) => {
|
||||
const { pageIds: projectPageIds } = props;
|
||||
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
commandPalette: { toggleCreatePageModal },
|
||||
} = useApplication();
|
||||
@@ -36,11 +39,8 @@ export const PagesListView: FC<IPagesListView> = (props) => {
|
||||
? PAGE_EMPTY_STATE_DETAILS[pageTab as keyof typeof PAGE_EMPTY_STATE_DETAILS]
|
||||
: PAGE_EMPTY_STATE_DETAILS["All"];
|
||||
|
||||
const emptyStateImage = getEmptyStateImagePath(
|
||||
"pages",
|
||||
currentPageTabDetails.key,
|
||||
currentUser?.theme.theme === "light"
|
||||
);
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("pages", currentPageTabDetails.key, isLightMode);
|
||||
|
||||
const isButtonVisible = currentPageTabDetails.key !== "archived" && currentPageTabDetails.key !== "favorites";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -14,6 +15,8 @@ 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 {
|
||||
@@ -22,7 +25,8 @@ export const RecentPagesList: FC = observer(() => {
|
||||
} = useUser();
|
||||
const { recentProjectPages } = useProjectPages();
|
||||
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode);
|
||||
|
||||
// FIXME: replace any with proper type
|
||||
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from "./overview";
|
||||
export * from "./navbar";
|
||||
export * from "./sidebar";
|
||||
|
||||
export * from "./profile-issues-filter";
|
||||
export * from "./sidebar";
|
||||
export * from "./time";
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
@@ -27,6 +28,9 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
workspaceSlug: string;
|
||||
userId: string;
|
||||
};
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
@@ -46,7 +50,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
}
|
||||
);
|
||||
|
||||
const emptyStateImage = getEmptyStateImagePath("profile", type, currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("profile", type, isLightMode);
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ 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
|
||||
@@ -34,16 +36,6 @@ 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",
|
||||
@@ -51,11 +43,7 @@ export const ProfileSidebar = observer(() => {
|
||||
},
|
||||
{
|
||||
label: "Timezone",
|
||||
value: (
|
||||
<span>
|
||||
{timeString} <span className="text-custom-text-200">{userProjectsData?.user_data.user_timezone}</span>
|
||||
</span>
|
||||
),
|
||||
value: <ProfileSidebarTime timeZone={userProjectsData?.user_data.user_timezone} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,17 @@
|
||||
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,
|
||||
@@ -22,7 +23,8 @@ export const ProjectCardList = observer(() => {
|
||||
} = useUser();
|
||||
const { workspaceProjectIds, searchedProjects, getProjectById } = useProject();
|
||||
|
||||
const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
Projects
|
||||
Your projects
|
||||
{open ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { FC } from "react";
|
||||
// hooks
|
||||
import { useCurrentTime } from "hooks/use-current-time";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
|
||||
export interface IUserGreetingsView {
|
||||
@@ -7,8 +10,8 @@ export interface IUserGreetingsView {
|
||||
|
||||
export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
|
||||
const { user } = props;
|
||||
|
||||
const currentTime = new Date();
|
||||
// current time hook
|
||||
const { currentTime } = useCurrentTime();
|
||||
|
||||
const hour = new Intl.DateTimeFormat("en-US", {
|
||||
hour12: false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -14,6 +15,8 @@ import { EUserProjectRoles } from "constants/project";
|
||||
export const ProjectViewsList = observer(() => {
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
commandPalette: { toggleCreateViewModal },
|
||||
@@ -43,7 +46,8 @@ export const ProjectViewsList = observer(() => {
|
||||
|
||||
const viewsList = projectViewIds.map((viewId) => getViewById(viewId));
|
||||
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode);
|
||||
|
||||
const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
|
||||
+11
-1
@@ -304,7 +304,17 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
},
|
||||
my_issues: {
|
||||
spreadsheet: {
|
||||
filters: ["priority", "state_group", "labels", "assignees", "created_by", "project", "start_date", "target_date"],
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
"subscriber",
|
||||
"project",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
display_properties: true,
|
||||
display_filters: {
|
||||
type: [null, "active", "backlog"],
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -264,6 +264,13 @@ 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,6 +1,7 @@
|
||||
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
|
||||
@@ -16,6 +17,8 @@ import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
commandPalette: { toggleCreateProjectModal },
|
||||
@@ -27,7 +30,8 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
} = useUser();
|
||||
const { workspaceProjectIds } = useProject();
|
||||
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode);
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
@@ -22,6 +23,8 @@ import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
const [createModal, setCreateModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
@@ -49,7 +52,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
},
|
||||
[handleCurrentLayout, setCycleTab]
|
||||
);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", currentUser?.theme.theme === "light");
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
|
||||
|
||||
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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";
|
||||
@@ -48,7 +49,9 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// states
|
||||
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
||||
// store
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
currentUser,
|
||||
currentUserLoader,
|
||||
@@ -94,7 +97,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", currentUser?.theme.theme === "light");
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
|
||||
@@ -31,7 +31,10 @@ export interface IIssueFilterHelperStore {
|
||||
filteredParams: TIssueParams[]
|
||||
): Partial<Record<TIssueParams, string | boolean>>;
|
||||
computedFilters(filters: IIssueFilterOptions): IIssueFilterOptions;
|
||||
computedDisplayFilters(displayFilters: IIssueDisplayFilterOptions): IIssueDisplayFilterOptions;
|
||||
computedDisplayFilters(
|
||||
displayFilters: IIssueDisplayFilterOptions,
|
||||
defaultValues?: IIssueDisplayFilterOptions
|
||||
): IIssueDisplayFilterOptions;
|
||||
computedDisplayProperties(filters: IIssueDisplayProperties): IIssueDisplayProperties;
|
||||
}
|
||||
|
||||
@@ -73,6 +76,8 @@ 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,
|
||||
@@ -147,20 +152,27 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||
* @param {IIssueDisplayFilterOptions} displayFilters
|
||||
* @returns {IIssueDisplayFilterOptions}
|
||||
*/
|
||||
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,
|
||||
});
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to apply the display properties on the issues
|
||||
|
||||
@@ -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, "issues");
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "my_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);
|
||||
displayFilters = this.computedDisplayFilters(_filters?.display_filters, { layout: "spreadsheet" });
|
||||
displayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
kanbanFilters = {
|
||||
group_by: _filters?.kanban_filters?.group_by || [],
|
||||
|
||||
Reference in New Issue
Block a user