merge conflicts resolved

This commit is contained in:
sriramveeraghanta 2023-08-16 13:00:58 +05:30
commit 9003c58d89
360 changed files with 13916 additions and 7344 deletions

View file

@ -14,17 +14,17 @@ type Props = {
export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) => {
let tooltipValue: string | number = "";
const renderAssigneeName = (assigneeId: string): string => {
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
if (!assignee) return "No assignee";
return assignee.assignees__display_name || "No assignee";
};
if (params.segment) {
if (DATE_KEYS.includes(params.segment)) tooltipValue = renderMonthAndYear(datum.id);
else if (params.segment === "assignees__email") {
const assignee = analytics.extras.assignee_details.find(
(a) => a.assignees__email === datum.id
);
if (assignee)
tooltipValue = assignee.assignees__first_name + " " + assignee.assignees__last_name;
else tooltipValue = "No assignees";
} else tooltipValue = datum.id;
else tooltipValue = datum.id;
} else {
if (DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue;
else tooltipValue = datum.id === "count" ? "Issue count" : "Estimate";
@ -49,7 +49,10 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
: ""
}`}
>
{tooltipValue}:
{params.segment === "assignees__id"
? renderAssigneeName(tooltipValue.toString())
: tooltipValue}
:
</span>
<span>{datum.value}</span>
</div>

View file

@ -29,6 +29,14 @@ export const AnalyticsGraph: React.FC<Props> = ({
yAxisKey,
fullScreen,
}) => {
const renderAssigneeName = (assigneeId: string): string => {
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
if (!assignee) return "?";
return assignee.assignees__display_name || "?";
};
const generateYAxisTickValues = () => {
if (!analytics) return [];
@ -70,17 +78,17 @@ export const AnalyticsGraph: React.FC<Props> = ({
height={fullScreen ? "400px" : "300px"}
margin={{
right: 20,
bottom: params.x_axis === "assignees__email" ? 50 : longestXAxisLabel.length * 5 + 20,
bottom: params.x_axis === "assignees__id" ? 50 : longestXAxisLabel.length * 5 + 20,
}}
axisBottom={{
tickSize: 0,
tickPadding: 10,
tickRotation: barGraphData.data.length > 7 ? -45 : 0,
renderTick:
params.x_axis === "assignees__email"
params.x_axis === "assignees__id"
? (datum) => {
const avatar = analytics.extras.assignee_details?.find(
(a) => a?.assignees__email === datum?.value
(a) => a?.assignees__display_name === datum?.value
)?.assignees__avatar;
if (avatar && avatar !== "")
@ -101,7 +109,11 @@ export const AnalyticsGraph: React.FC<Props> = ({
<g transform={`translate(${datum.x},${datum.y})`}>
<circle cy={18} r={8} fill="#374151" />
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
{datum.value && datum.value !== "None"
{params.x_axis === "assignees__id"
? datum.value && datum.value !== "None"
? renderAssigneeName(datum.value)[0].toUpperCase()
: "?"
: datum.value && datum.value !== "None"
? `${datum.value}`.toUpperCase()[0]
: "?"}
</text>

View file

@ -277,9 +277,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Lead</h6>
<span>
{cycleDetails.owned_by?.first_name} {cycleDetails.owned_by?.last_name}
</span>
<span>{cycleDetails.owned_by?.display_name}</span>
</div>
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Start Date</h6>
@ -305,10 +303,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
<div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Lead</h6>
<span>
{moduleDetails.lead_detail?.first_name}{" "}
{moduleDetails.lead_detail?.last_name}
</span>
<span>{moduleDetails.lead_detail?.display_name}</span>
</div>
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Start Date</h6>

View file

@ -22,15 +22,12 @@ type Props = {
};
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => {
const renderAssigneeName = (email: string): string => {
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__email === email);
const renderAssigneeName = (assigneeId: string): string => {
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
if (!assignee) return "No assignee";
if (assignee.assignees__first_name !== "")
return assignee.assignees__first_name + " " + assignee.assignees__last_name;
return email;
return assignee.assignees__display_name || "No assignee";
};
return (
@ -65,10 +62,10 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
}}
/>
)}
{DATE_KEYS.includes(params.segment ?? "")
? renderMonthAndYear(key)
: params.segment === "assignees__email"
{params.segment === "assignees__id"
? renderAssigneeName(key)
: DATE_KEYS.includes(params.segment ?? "")
? renderMonthAndYear(key)
: key}
</div>
</th>
@ -108,7 +105,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
}}
/>
)}
{params.x_axis === "assignees__email"
{params.x_axis === "assignees__id"
? renderAssigneeName(`${item.name}`)
: addSpaceIfCamelCase(`${item.name}`)}
</td>

View file

@ -1,22 +1,27 @@
type Props = {
users: {
avatar: string | null;
email: string | null;
display_name: string | null;
firstName: string;
lastName: string;
count: number;
id: string;
}[];
title: string;
workspaceSlug: string;
};
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title, workspaceSlug }) => (
<div className="p-3 border border-custom-border-200 rounded-[10px]">
<h6 className="text-base font-medium">{title}</h6>
{users.length > 0 ? (
<div className="mt-3 space-y-3">
{users.map((user) => (
<div
key={user.email ?? "None"}
<a
key={user.display_name ?? "None"}
href={`/${workspaceSlug}/profile/${user.id}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-start justify-between gap-4 text-xs"
>
<div className="flex items-center gap-2">
@ -25,20 +30,20 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
<img
src={user.avatar}
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
alt={user.email ?? "None"}
alt={user.display_name ?? "None"}
/>
</div>
) : (
<div className="grid place-items-center flex-shrink-0 rounded-full bg-gray-700 text-[11px] capitalize text-white h-4 w-4">
{user.firstName !== "" ? user.firstName[0] : "?"}
{user.display_name !== "" ? user?.display_name?.[0] : "?"}
</div>
)}
<span className="break-words text-custom-text-200">
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"}
{user.display_name !== "" ? `${user.display_name}` : "No assignee"}
</span>
</div>
<span className="flex-shrink-0">{user.count}</span>
</div>
</a>
))}
</div>
) : (

View file

@ -56,22 +56,26 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
<AnalyticsLeaderboard
users={defaultAnalytics.most_issue_created_user?.map((user) => ({
avatar: user?.created_by__avatar,
email: user?.created_by__email,
firstName: user?.created_by__first_name,
lastName: user?.created_by__last_name,
display_name: user?.created_by__display_name,
count: user?.count,
id: user?.created_by__id,
}))}
title="Most issues created"
workspaceSlug={workspaceSlug?.toString() ?? ""}
/>
<AnalyticsLeaderboard
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
avatar: user?.assignees__avatar,
email: user?.assignees__email,
firstName: user?.assignees__first_name,
lastName: user?.assignees__last_name,
display_name: user?.assignees__display_name,
count: user?.count,
id: user?.assignees__id,
}))}
title="Most issues closed"
workspaceSlug={workspaceSlug?.toString() ?? ""}
/>
<div className={fullScreen ? "md:col-span-2" : ""}>
<AnalyticsYearWiseIssues defaultAnalytics={defaultAnalytics} />

View file

@ -16,23 +16,20 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
{defaultAnalytics.pending_issue_user.length > 0 ? (
<BarGraph
data={defaultAnalytics.pending_issue_user}
indexBy="assignees__email"
indexBy="assignees__display_name"
keys={["count"]}
height="250px"
colors={() => `#f97316`}
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
tooltip={(datum) => {
const assignee = defaultAnalytics.pending_issue_user.find(
(a) => a.assignees__email === `${datum.indexValue}`
(a) => a.assignees__display_name === `${datum.indexValue}`
);
return (
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
<span className="font-medium text-custom-text-200">
{assignee
? assignee.assignees__first_name + " " + assignee.assignees__last_name
: "No assignee"}
:{" "}
{assignee ? assignee.assignees__display_name : "No assignee"}:{" "}
</span>
{datum.value}
</div>

View file

@ -7,6 +7,8 @@ import { useTheme } from "next-themes";
import { SettingIcon } from "components/icons";
import userService from "services/user.service";
import useUser from "hooks/use-user";
// helper
import { unsetCustomCssVariables } from "helpers/theme.helper";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@ -22,15 +24,17 @@ export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
const updateUserTheme = (newTheme: string) => {
if (!user) return;
unsetCustomCssVariables();
setTheme(newTheme);
mutateUser((prevData) => {
mutateUser((prevData: any) => {
if (!prevData) return prevData;
return {
...prevData,
theme: {
...prevData.theme,
...prevData?.theme,
theme: newTheme,
},
};

View file

@ -354,8 +354,8 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Item
key={item.id}
onSelect={() => {
router.push(currentSection.path(item));
setIsPaletteOpen(false);
router.push(currentSection.path(item));
}}
value={`${key}-${item?.name}`}
className="focus:outline-none"
@ -379,6 +379,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Issue actions">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
setPlaceholder("Change state...");
setSearchTerm("");
setPages([...pages, "change-issue-state"]);
@ -460,6 +461,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Issue">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
const e = new KeyboardEvent("keydown", {
key: "c",
});
@ -479,6 +481,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Project">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
const e = new KeyboardEvent("keydown", {
key: "p",
});
@ -500,6 +503,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Cycle">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
const e = new KeyboardEvent("keydown", {
key: "q",
});
@ -517,6 +521,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Module">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
const e = new KeyboardEvent("keydown", {
key: "m",
});
@ -534,6 +539,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="View">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
const e = new KeyboardEvent("keydown", {
key: "v",
});
@ -551,6 +557,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Page">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
const e = new KeyboardEvent("keydown", {
key: "d",
});
@ -568,11 +575,12 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
{projectDetails && projectDetails.inbox_view && (
<Command.Group heading="Inbox">
<Command.Item
onSelect={() =>
onSelect={() => {
setIsPaletteOpen(false);
redirect(
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
)
}
);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
@ -731,12 +739,21 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/import-export`)}
onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Import/Export
Import
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Export
</div>
</Command.Item>
</>

View file

@ -1,11 +1,8 @@
import React, { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// hooks
import useTheme from "hooks/use-theme";
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
// components
@ -24,8 +21,14 @@ import issuesService from "services/issues.service";
import inboxService from "services/inbox.service";
// fetch keys
import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { observable } from "mobx";
import { observer } from "mobx-react-lite";
export const CommandPalette: React.FC = observer(() => {
const store: any = useMobxStore();
export const CommandPalette: React.FC = () => {
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
@ -43,13 +46,12 @@ export const CommandPalette: React.FC = () => {
const { user } = useUser();
const { setToastAlert } = useToast();
const { toggleCollapsed } = useTheme();
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () =>
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
@ -74,53 +76,52 @@ export const CommandPalette: React.FC = () => {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const singleShortcutKeys = ["p", "v", "d", "h", "q", "m"];
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
if (!key) return;
const keyPressed = key.toLowerCase();
const cmdClicked = ctrlKey || metaKey;
// if on input, textarea or editor, don't do anything
if (
!(e.target instanceof HTMLTextAreaElement) &&
!(e.target instanceof HTMLInputElement) &&
!(e.target as Element).classList?.contains("remirror-editor")
) {
if ((ctrlKey || metaKey) && keyPressed === "k") {
e.preventDefault();
setIsPaletteOpen(true);
} else if ((ctrlKey || metaKey) && keyPressed === "c") {
if (altKey) {
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLInputElement ||
(e.target as Element).classList?.contains("ProseMirror")
)
return;
if (cmdClicked) {
if (keyPressed === "k") {
e.preventDefault();
setIsPaletteOpen(true);
} else if (keyPressed === "c" && altKey) {
e.preventDefault();
copyIssueUrlToClipboard();
} else if (keyPressed === "b") {
e.preventDefault();
store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
}
} else {
if (keyPressed === "c") {
setIsIssueModalOpen(true);
} else if (keyPressed === "p") {
setIsProjectModalOpen(true);
} else if (keyPressed === "v") {
setIsCreateViewModalOpen(true);
} else if (keyPressed === "d") {
setIsCreateUpdatePageModalOpen(true);
} else if (keyPressed === "h") {
setIsShortcutsModalOpen(true);
} else if (keyPressed === "q") {
setIsCreateCycleModalOpen(true);
} else if (keyPressed === "m") {
setIsCreateModuleModalOpen(true);
} else if (keyPressed === "backspace" || keyPressed === "delete") {
e.preventDefault();
setIsBulkDeleteIssuesModalOpen(true);
}
} else if (keyPressed === "c") {
e.preventDefault();
setIsIssueModalOpen(true);
} else if ((ctrlKey || metaKey) && keyPressed === "b") {
e.preventDefault();
toggleCollapsed();
} else if (key === "Delete") {
e.preventDefault();
setIsBulkDeleteIssuesModalOpen(true);
} else if (
singleShortcutKeys.includes(keyPressed) &&
(ctrlKey || metaKey || altKey || shiftKey)
) {
e.preventDefault();
} else if (keyPressed === "p") {
setIsProjectModalOpen(true);
} else if (keyPressed === "v") {
setIsCreateViewModalOpen(true);
} else if (keyPressed === "d") {
setIsCreateUpdatePageModalOpen(true);
} else if (keyPressed === "h") {
setIsShortcutsModalOpen(true);
} else if (keyPressed === "q") {
setIsCreateCycleModalOpen(true);
} else if (keyPressed === "m") {
setIsCreateModuleModalOpen(true);
}
}
},
[toggleCollapsed, copyIssueUrlToClipboard]
[copyIssueUrlToClipboard]
);
useEffect(() => {
@ -195,4 +196,4 @@ export const CommandPalette: React.FC = () => {
/>
</>
);
};
})

View file

@ -34,15 +34,12 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue,
const options =
members?.map(({ member }) => ({
value: member.id,
query:
(member.first_name && member.first_name !== "" ? member.first_name : member.email) +
" " +
member.last_name ?? "",
query: member.display_name,
content: (
<>
<div className="flex items-center gap-2">
<Avatar user={member} />
{member.first_name && member.first_name !== "" ? member.first_name : member.email}
{member.display_name}
</div>
{issue.assignees.includes(member.id) && (
<div>

View file

@ -35,6 +35,22 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
);
};
const UserLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<a
href={`/${workspaceSlug}/profile/${activity.new_identifier ?? activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center hover:underline"
>
{activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value}
</a>
);
};
const activityDetails: {
[key: string]: {
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
@ -46,8 +62,7 @@ const activityDetails: {
if (activity.old_value === "")
return (
<>
added a new assignee{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
added a new assignee <UserLink activity={activity} />
{showIssue && (
<>
{" "}
@ -60,8 +75,7 @@ const activityDetails: {
else
return (
<>
removed the assignee{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
removed the assignee <UserLink activity={activity} />
{showIssue && (
<>
{" "}
@ -428,6 +442,40 @@ const activityDetails: {
),
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
},
start_date: {
message: (activity, showIssue) => {
if (!activity.new_value)
return (
<>
removed the start date
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
else
return (
<>
set the start date to{" "}
<span className="font-medium text-custom-text-100">
{renderShortDateWithYearFormat(activity.new_value)}
</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
);
},
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
},
state: {
message: (activity, showIssue) => (
<>

View file

@ -157,10 +157,10 @@ export const FiltersList: React.FC<Props> = ({
return (
<div
key={memberId}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
>
<Avatar user={member} />
<span>{member?.first_name}</span>
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
onClick={() =>
@ -184,7 +184,7 @@ export const FiltersList: React.FC<Props> = ({
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
>
<Avatar user={member} />
<span>{member?.first_name}</span>
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
onClick={() =>

View file

@ -113,49 +113,51 @@ export const IssuesFilterView: React.FC = () => {
))}
</div>
)}
<SelectFilters
filters={filters}
onSelect={(option) => {
const key = option.key as keyof typeof filters;
{issueView !== "gantt_chart" && (
<SelectFilters
filters={filters}
onSelect={(option) => {
const key = option.key as keyof typeof filters;
if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters.target_date ?? [],
option.value
);
setFilters({
target_date: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);
if (valueExists)
setFilters(
{
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
},
!Boolean(viewId)
if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters.target_date ?? [],
option.value
);
else
setFilters(
{
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
},
!Boolean(viewId)
);
}
}}
direction="left"
height="rg"
/>
setFilters({
target_date: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);
if (valueExists)
setFilters(
{
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
},
!Boolean(viewId)
);
else
setFilters(
{
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
},
!Boolean(viewId)
);
}
}}
direction="left"
height="rg"
/>
)}
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
@ -177,8 +179,9 @@ export const IssuesFilterView: React.FC = () => {
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
@ -206,34 +209,34 @@ export const IssuesFilterView: React.FC = () => {
</CustomMenu>
</div>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" &&
option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
)}
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
</>
</div>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
@ -263,16 +266,19 @@ export const IssuesFilterView: React.FC = () => {
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show sub-issues</h4>
<div className="w-28">
<ToggleSwitch
value={showSubIssues}
onChange={() => setShowSubIssues(!showSubIssues)}
/>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show sub-issues</h4>
<div className="w-28">
<ToggleSwitch
value={showSubIssues}
onChange={() => setShowSubIssues(!showSubIssues)}
/>
</div>
</div>
)}
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
@ -282,6 +288,10 @@ export const IssuesFilterView: React.FC = () => {
/>
</div>
</div>
)}
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
<div className="relative flex justify-end gap-x-3">
<button type="button" onClick={() => resetFilterToDefault()}>
Reset to default
@ -294,47 +304,48 @@ export const IssuesFilterView: React.FC = () => {
Set as default
</button>
</div>
</>
)}
)}
</div>
<div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
{Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
{issueView !== "gantt_chart" && (
<div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
{Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
if (
issueView === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
)
return null;
if (
issueView === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
)
return null;
if (
issueView !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;
if (
issueView !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;
return (
<button
key={key}
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200"
}`}
onClick={() => setProperties(key as keyof Properties)}
>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
);
})}
return (
<button
key={key}
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200"
}`}
onClick={() => setProperties(key as keyof Properties)}
>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
);
})}
</div>
</div>
</div>
)}
</div>
</Popover.Panel>
</Transition>

View file

@ -27,8 +27,8 @@ const unsplashEnabled =
const tabOptions = [
{
key: "unsplash",
title: "Unsplash",
key: "images",
title: "Images",
},
{
key: "upload",

View file

@ -1,7 +1,6 @@
import { useEffect, useState, forwardRef, useRef } from "react";
import React, { useEffect, useState, forwardRef, useRef } from "react";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
// react-hook-form
import { useForm } from "react-hook-form";
@ -15,6 +14,7 @@ import useUserAuth from "hooks/use-user-auth";
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
import { IIssue, IPageBlock } from "types";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
type Props = {
isOpen: boolean;
handleClose: () => void;
@ -32,17 +32,11 @@ type FormData = {
task: string;
};
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
});
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
const WrappedRemirrorRichTextEditor = forwardRef<IRemirrorRichTextEditor, IRemirrorRichTextEditor>(
(props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
TiptapEditor.displayName = "TiptapEditor";
export const GptAssistantModal: React.FC<Props> = ({
isOpen,
@ -151,10 +145,10 @@ export const GptAssistantModal: React.FC<Props> = ({
}`}
>
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
<div className="remirror-section text-sm">
<div id="tiptap-container" className="text-sm">
Content:
<WrappedRemirrorRichTextEditor
value={htmlContent ?? <p>{content}</p>}
<TiptapEditor
value={htmlContent ?? `<p>${content}</p>`}
customClassName="-m-3"
noBorder
borderOnFocus={false}
@ -166,7 +160,7 @@ export const GptAssistantModal: React.FC<Props> = ({
{response !== "" && (
<div className="page-block-section text-sm">
Response:
<RemirrorRichTextEditor
<Tiptap
value={`<p>${response}</p>`}
customClassName="-mx-3 -my-3"
noBorder

View file

@ -131,10 +131,10 @@ export const ImageUploadModal: React.FC<Props> = ({
Upload Image
</Dialog.Title>
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center gap-3">
<div
{...getRootProps()}
className={`relative grid h-80 w-full cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-custom-primary focus:ring-offset-2 ${
className={`relative grid h-80 w-80 cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-custom-primary focus:ring-offset-2 ${
(image === null && isDragActive) || !value
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
: ""

View file

@ -62,7 +62,7 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
by{" "}
{link.created_by_detail.is_bot
? link.created_by_detail.first_name + " Bot"
: link.created_by_detail.email}
: link.created_by_detail.display_name}
</p>
</div>
</a>

View file

@ -133,9 +133,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
avatar: assignee.avatar ?? "",
first_name: assignee.first_name ?? "",
last_name: assignee.last_name ?? "",
display_name: assignee.display_name ?? "",
}}
/>
<span>{assignee.first_name}</span>
<span>{assignee.display_name}</span>
</div>
}
completed={assignee.completed_issues}

View file

@ -21,13 +21,21 @@ import { ICustomTheme } from "types";
type Props = {
name: keyof ICustomTheme;
position?: "left" | "right";
watch: UseFormWatch<any>;
setValue: UseFormSetValue<any>;
error: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
register: UseFormRegister<any>;
};
export const ColorPickerInput: React.FC<Props> = ({ name, watch, setValue, error, register }) => {
export const ColorPickerInput: React.FC<Props> = ({
name,
position = "left",
watch,
setValue,
error,
register,
}) => {
const handleColorChange = (newColor: ColorResult) => {
const { hex } = newColor;
setValue(name, hex);
@ -104,7 +112,11 @@ export const ColorPickerInput: React.FC<Props> = ({ name, watch, setValue, error
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute bottom-8 right-0 z-20 mt-1 max-w-xs px-2 sm:px-0">
<Popover.Panel
className={`absolute bottom-8 z-20 mt-1 max-w-xs px-2 sm:px-0 ${
position === "right" ? "left-0" : "right-0"
}`}
>
<SketchPicker color={watch(name)} onChange={handleColorChange} />
</Popover.Panel>
</Transition>

View file

@ -4,17 +4,15 @@ import { useTheme } from "next-themes";
import { useForm } from "react-hook-form";
// hooks
import useUser from "hooks/use-user";
// ui
import { PrimaryButton } from "components/ui";
import { ColorPickerInput } from "components/core";
// services
import userService from "services/user.service";
// helper
import { applyTheme } from "helpers/theme.helper";
// types
import { ICustomTheme } from "types";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
preLoadedData?: Partial<ICustomTheme> | null;
@ -31,9 +29,11 @@ const defaultValues: ICustomTheme = {
theme: "custom",
};
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
const [darkPalette, setDarkPalette] = useState(false);
export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData }) => {
const store: any = useMobxStore();
const { setTheme } = useTheme();
const [darkPalette, setDarkPalette] = useState(false);
const {
register,
formState: { errors, isSubmitting },
@ -44,11 +44,14 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
} = useForm<ICustomTheme>({
defaultValues,
});
useEffect(() => {
reset({
...defaultValues,
...preLoadedData,
});
}, [preLoadedData, reset]);
const { setTheme } = useTheme();
const { mutateUser } = useUser();
const handleFormSubmit = async (formData: ICustomTheme) => {
const handleUpdateTheme = async (formData: any) => {
const payload: ICustomTheme = {
background: formData.background,
text: formData.text,
@ -60,34 +63,14 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
theme: "custom",
};
await userService
.updateUser({
theme: payload,
})
.then((res) => {
mutateUser((prevData) => {
if (!prevData) return prevData;
setTheme("custom");
return { ...prevData, ...res };
}, false);
setTheme("custom");
applyTheme(payload.palette, darkPalette);
})
.catch((err) => console.log(err));
return store.user
.updateCurrentUserSettings({ theme: payload })
.then((response: any) => response)
.catch((error: any) => error);
};
const handleUpdateTheme = async (formData: any) => {
await handleFormSubmit({ ...formData, darkPalette });
};
useEffect(() => {
reset({
...defaultValues,
...preLoadedData,
});
}, [preLoadedData, reset]);
return (
<form onSubmit={handleSubmit(handleUpdateTheme)}>
<div className="space-y-5">
@ -100,6 +83,7 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
</h3>
<ColorPickerInput
name="background"
position="right"
error={errors.background}
watch={watch}
setValue={setValue}
@ -137,6 +121,7 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
</h3>
<ColorPickerInput
name="sidebarBackground"
position="right"
error={errors.sidebarBackground}
watch={watch}
setValue={setValue}
@ -166,4 +151,4 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
</div>
</form>
);
};
});

View file

@ -1,9 +1,5 @@
import { useState, useEffect } from "react";
// next-themes
import { useTheme } from "next-themes";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// constants
@ -13,6 +9,10 @@ import { CustomSelect } from "components/ui";
// types
import { ICustomTheme } from "types";
import { unsetCustomCssVariables } from "helpers/theme.helper";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
setPreLoadedData: React.Dispatch<React.SetStateAction<ICustomTheme | null>>;
@ -20,63 +20,30 @@ type Props = {
setCustomThemeSelectorOptions: React.Dispatch<React.SetStateAction<boolean>>;
};
export const ThemeSwitch: React.FC<Props> = ({
setPreLoadedData,
customThemeSelectorOptions,
setCustomThemeSelectorOptions,
}) => {
const [mounted, setMounted] = useState(false);
export const ThemeSwitch: React.FC<Props> = observer(
({ setPreLoadedData, customThemeSelectorOptions, setCustomThemeSelectorOptions }) => {
const store: any = useMobxStore();
const { theme, setTheme } = useTheme();
const { user, mutateUser } = useUser();
const { theme, setTheme } = useTheme();
const { user, mutateUser } = useUser();
const updateUserTheme = (newTheme: string) => {
if (!user) return;
setTheme(newTheme);
return store.user
.updateCurrentUserSettings({ theme: { ...user.theme, theme: newTheme } })
.then((response: any) => response)
.catch((error: any) => error);
};
const updateUserTheme = (newTheme: string) => {
if (!user) return;
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
setTheme(newTheme);
mutateUser((prevData) => {
if (!prevData) return prevData;
return {
...prevData,
theme: {
...prevData.theme,
theme: newTheme,
},
};
}, false);
userService.updateUser({
theme: {
...user.theme,
theme: newTheme,
},
});
};
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
return (
<CustomSelect
value={theme}
label={
currentThemeObj ? (
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: currentThemeObj.icon.border,
}}
>
return (
<CustomSelect
value={theme}
label={
currentThemeObj ? (
<div className="flex items-center gap-2">
<div
className="h-full w-1/2 rounded-l-full"
style={{
@ -91,53 +58,45 @@ export const ThemeSwitch: React.FC<Props> = ({
}}
/>
</div>
{currentThemeObj.label}
</div>
) : (
"Select your theme"
)
}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (user?.theme.palette) {
setPreLoadedData({
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarBackground:
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
) : (
"Select your theme"
)
}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (user?.theme.palette) {
setPreLoadedData({
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarBackground:
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
}
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
} else {
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
unsetCustomCssVariables();
}
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
} else {
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
unsetCustomCssVariables();
}
updateUserTheme(value);
document.documentElement.style.setProperty("--color-scheme", type);
}}
input
width="w-full"
position="right"
>
{THEMES_OBJ.map(({ value, label, type, icon }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: icon.border,
}}
>
updateUserTheme(value);
document.documentElement.style.setProperty("--color-scheme", type);
}}
input
width="w-full"
position="right"
>
{THEMES_OBJ.map(({ value, label, type, icon }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
<div className="flex items-center gap-2">
<div
className="h-full w-1/2 rounded-l-full"
style={{
@ -152,10 +111,9 @@ export const ThemeSwitch: React.FC<Props> = ({
}}
/>
</div>
{label}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
);
};
</CustomSelect.Option>
))}
</CustomSelect>
);
}
);

View file

@ -10,7 +10,7 @@ import projectService from "services/project.service";
// hooks
import useProjects from "hooks/use-projects";
// component
import { Avatar } from "components/ui";
import { Avatar, Icon } from "components/ui";
// icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
@ -81,10 +81,7 @@ export const BoardHeader: React.FC<Props> = ({
break;
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
title =
member?.first_name && member.first_name !== ""
? `${member.first_name} ${member.last_name}`
: member?.email ?? "";
title = member?.display_name ?? "";
break;
}
@ -143,24 +140,22 @@ export const BoardHeader: React.FC<Props> = ({
>
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
<div
className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${
className={`flex cursor-pointer items-center gap-x-2 max-w-[316px] ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`}
>
<span className="flex items-center">{getGroupIcon()}</span>
<h2
className="text-lg font-semibold capitalize truncate"
className={`text-lg font-semibold truncate ${
selectedGroup === "created_by" ? "" : "capitalize"
}`}
style={{
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
}}
>
{getGroupTitle()}
</h2>
<span
className={`${
isCollapsed ? "ml-0.5" : ""
} min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`}
>
<span className={`${isCollapsed ? "ml-0.5" : ""} py-1 text-center text-sm`}>
{groupedIssues?.[groupTitle].length ?? 0}
</span>
</div>
@ -175,9 +170,12 @@ export const BoardHeader: React.FC<Props> = ({
}}
>
{isCollapsed ? (
<ArrowsPointingInIcon className="h-4 w-4" />
<Icon
iconName="close_fullscreen"
className="text-base font-medium text-custom-text-900"
/>
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
)}
</button>
{!disableUserActions && selectedGroup !== "created_by" && (

View file

@ -24,6 +24,7 @@ import {
ViewEstimateSelect,
ViewIssueLabel,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
} from "components/issues";
// ui
@ -124,7 +125,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
);
} else {
mutateIssues(
(prevData) =>
(prevData: any) =>
handleIssuesMutation(
formData,
groupTitle ?? "",
@ -231,7 +232,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
</a>
</ContextMenu>
<div
className={`mb-3 rounded bg-custom-background-90 shadow ${
className={`mb-3 rounded bg-custom-background-100 shadow ${
snapshot.isDragging ? "border-2 border-custom-primary shadow-lg" : ""
}`}
ref={provided.innerRef}
@ -300,10 +301,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<h5 className="text-sm break-words line-clamp-3">{issue.name}</h5>
<h5 className="text-sm break-words line-clamp-2">{issue.name}</h5>
</a>
</Link>
<div className="relative mt-2.5 flex flex-wrap items-center gap-2 text-xs">
<div className="mt-2.5 flex overflow-x-scroll items-center gap-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
@ -322,6 +323,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned
/>
)}
{properties.start_date && issue.start_date && (
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
@ -338,6 +347,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
customButton
user={user}
selfPositioned
/>

View file

@ -21,6 +21,7 @@ import {
ViewEstimateSelect,
ViewLabelSelect,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
} from "components/issues";
// icons
@ -230,7 +231,14 @@ export const SingleCalendarIssue: React.FC<Props> = ({
user={user}
/>
)}
{properties.start_date && issue.start_date && (
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}

View file

@ -16,6 +16,7 @@ import {
ViewEstimateSelect,
ViewIssueLabel,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
} from "components/issues";
// ui
@ -107,7 +108,7 @@ export const SingleListIssue: React.FC<Props> = ({
);
} else {
mutateIssues(
(prevData) =>
(prevData: any) =>
handleIssuesMutation(
formData,
groupTitle ?? "",
@ -244,6 +245,14 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.start_date && issue.start_date && (
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}

View file

@ -96,10 +96,7 @@ export const SingleList: React.FC<Props> = ({
break;
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
title =
member?.first_name && member.first_name !== ""
? `${member.first_name} ${member.last_name}`
: member?.email ?? "";
title = member?.display_name ?? "";
break;
}
@ -163,7 +160,11 @@ export const SingleList: React.FC<Props> = ({
<div className="flex items-center">{getGroupIcon()}</div>
)}
{selectedGroup !== null ? (
<h2 className="text-sm font-semibold capitalize leading-6 text-custom-text-100">
<h2
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
selectedGroup === "created_by" ? "" : "capitalize"
}`}
>
{getGroupTitle()}
</h2>
) : (

View file

@ -12,6 +12,7 @@ import {
ViewEstimateSelect,
ViewIssueLabel,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
} from "components/issues";
import { Popover2 } from "@blueprintjs/popover2";
@ -315,6 +316,19 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
</div>
)}
{properties.start_date && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.due_date && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewDueDateSelect

View file

@ -361,14 +361,14 @@ export const ActiveCycleDetails: React.FC = () => {
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.first_name}
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{cycle.owned_by.first_name.charAt(0)}
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.first_name}</span>
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
</div>
{cycle.assignees.length > 0 && (

View file

@ -88,9 +88,10 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
avatar: assignee.avatar ?? "",
first_name: assignee.first_name ?? "",
last_name: assignee.last_name ?? "",
display_name: assignee.display_name ?? "",
}}
/>
<span>{assignee.first_name}</span>
<span>{assignee.display_name}</span>
</div>
}
completed={assignee.completed_issues}

View file

@ -1,21 +1,28 @@
import { FC } from "react";
// next imports
import Link from "next/link";
import { useRouter } from "next/router";
import { KeyedMutator } from "swr";
// services
import cyclesService from "services/cycles.service";
// hooks
import useUser from "hooks/use-user";
// components
import { GanttChartRoot } from "components/gantt-chart";
// ui
import { Tooltip } from "components/ui";
import { CycleGanttBlock, GanttChartRoot, IBlockUpdateData } from "components/gantt-chart";
// types
import { ICycle } from "types";
type Props = {
cycles: ICycle[];
mutateCycles: KeyedMutator<ICycle[]>;
};
export const CyclesListGanttChartView: FC<Props> = ({ cycles }) => {
export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug } = router.query;
const { user } = useUser();
// rendering issues on gantt sidebar
const GanttSidebarBlockView = ({ data }: any) => (
@ -28,53 +35,63 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles }) => {
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: { data: ICycle }) => (
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${data?.id}`}>
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
<div
className="flex-shrink-0 w-[4px] h-full"
style={{ backgroundColor: "rgb(var(--color-primary-100))" }}
/>
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
{data?.name}
</div>
</Tooltip>
</a>
</Link>
);
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
if (!workspaceSlug || !user) return;
// handle gantt issue start date and target date
const handleUpdateDates = async (data: any) => {
const payload = {
id: data?.id,
start_date: data?.start_date,
target_date: data?.target_date,
};
mutateCycles((prevData: any) => {
if (!prevData) return prevData;
const newList = prevData.map((p: any) => ({
...p,
...(p.id === cycle.id
? {
start_date: payload.start_date ? payload.start_date : p.start_date,
target_date: payload.target_date ? payload.target_date : p.end_date,
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
}
: {}),
}));
if (payload.sort_order) {
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
}
return newList;
}, false);
const newPayload: any = { ...payload };
if (newPayload.sort_order && payload.sort_order)
newPayload.sort_order = payload.sort_order.newSortOrder;
cyclesService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload, user);
};
const blockFormat = (blocks: any) =>
const blockFormat = (blocks: ICycle[]) =>
blocks && blocks.length > 0
? blocks.map((_block: any) => {
if (_block?.start_date && _block.target_date) console.log("_block", _block);
return {
start_date: new Date(_block.created_at),
target_date: new Date(_block.updated_at),
data: _block,
};
})
? blocks
.filter((b) => b.start_date && b.end_date)
.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: new Date(block.start_date ?? ""),
target_date: new Date(block.end_date ?? ""),
}))
: [];
return (
<div className="w-full h-full overflow-y-auto">
<GanttChartRoot
title={"Cycles"}
title="Cycles"
loaderTitle="Cycles"
blocks={cycles ? blockFormat(cycles) : null}
blockUpdateHandler={handleUpdateDates}
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
blockRender={(data: any) => <CycleGanttBlock cycle={data as ICycle} />}
enableLeftDrag={false}
enableRightDrag={false}
/>
</div>
);

View file

@ -17,7 +17,7 @@ export const AllCyclesList: React.FC<Props> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: allCyclesList } = useSWR(
const { data: allCyclesList, mutate } = useSWR(
workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null,
workspaceSlug && projectId
? () =>
@ -25,5 +25,5 @@ export const AllCyclesList: React.FC<Props> = ({ viewType }) => {
: null
);
return <CyclesView cycles={allCyclesList} viewType={viewType} />;
return <CyclesView cycles={allCyclesList} mutateCycles={mutate} viewType={viewType} />;
};

View file

@ -17,7 +17,7 @@ export const CompletedCyclesList: React.FC<Props> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: completedCyclesList } = useSWR(
const { data: completedCyclesList, mutate } = useSWR(
workspaceSlug && projectId ? COMPLETED_CYCLES_LIST(projectId.toString()) : null,
workspaceSlug && projectId
? () =>
@ -29,5 +29,5 @@ export const CompletedCyclesList: React.FC<Props> = ({ viewType }) => {
: null
);
return <CyclesView cycles={completedCyclesList} viewType={viewType} />;
return <CyclesView cycles={completedCyclesList} mutateCycles={mutate} viewType={viewType} />;
};

View file

@ -17,7 +17,7 @@ export const DraftCyclesList: React.FC<Props> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: draftCyclesList } = useSWR(
const { data: draftCyclesList, mutate } = useSWR(
workspaceSlug && projectId ? DRAFT_CYCLES_LIST(projectId.toString()) : null,
workspaceSlug && projectId
? () =>
@ -25,5 +25,5 @@ export const DraftCyclesList: React.FC<Props> = ({ viewType }) => {
: null
);
return <CyclesView cycles={draftCyclesList} viewType={viewType} />;
return <CyclesView cycles={draftCyclesList} mutateCycles={mutate} viewType={viewType} />;
};

View file

@ -17,7 +17,7 @@ export const UpcomingCyclesList: React.FC<Props> = ({ viewType }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: upcomingCyclesList } = useSWR(
const { data: upcomingCyclesList, mutate } = useSWR(
workspaceSlug && projectId ? UPCOMING_CYCLES_LIST(projectId.toString()) : null,
workspaceSlug && projectId
? () =>
@ -29,5 +29,5 @@ export const UpcomingCyclesList: React.FC<Props> = ({ viewType }) => {
: null
);
return <CyclesView cycles={upcomingCyclesList} viewType={viewType} />;
return <CyclesView cycles={upcomingCyclesList} mutateCycles={mutate} viewType={viewType} />;
};

View file

@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { KeyedMutator, mutate } from "swr";
// services
import cyclesService from "services/cycles.service";
@ -35,10 +35,11 @@ import {
type Props = {
cycles: ICycle[] | undefined;
mutateCycles: KeyedMutator<ICycle[]>;
viewType: string | null;
};
export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType }) => {
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(null);
@ -202,7 +203,7 @@ export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
))}
</div>
) : (
<CyclesListGanttChartView cycles={cycles ?? []} />
<CyclesListGanttChartView cycles={cycles ?? []} mutateCycles={mutateCycles} />
)
) : (
<div className="h-full grid place-items-center text-center">

View file

@ -1,20 +1,27 @@
import { FC } from "react";
// next imports
import Link from "next/link";
import { useRouter } from "next/router";
// components
import { GanttChartRoot } from "components/gantt-chart";
// ui
import { Tooltip } from "components/ui";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user";
import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view";
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
// components
import {
GanttChartRoot,
IssueGanttBlock,
renderIssueBlocksStructure,
} from "components/gantt-chart";
// types
import { IIssue } from "types";
type Props = {};
export const CycleIssuesGanttChartView: FC<Props> = ({}) => {
export const CycleIssuesGanttChartView = () => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const { orderBy } = useIssuesView();
const { user } = useUser();
const { ganttIssues, mutateGanttIssues } = useGanttChartCycleIssues(
workspaceSlug as string,
projectId as string,
@ -32,77 +39,18 @@ export const CycleIssuesGanttChartView: FC<Props> = ({}) => {
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: any) => (
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${data?.id}`}>
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm">
<div
className="flex-shrink-0 w-[4px] h-full"
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
/>
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
{data?.name}
</div>
</Tooltip>
{data.infoToggle && (
<Tooltip
tooltipContent={`No due-date set, rendered according to last updated date.`}
className={`z-[999999]`}
>
<div className="flex-shrink-0 mx-2 w-[18px] h-[18px] overflow-hidden flex justify-center items-center">
<span className="material-symbols-rounded text-custom-text-200 text-[18px]">
info
</span>
</div>
</Tooltip>
)}
</a>
</Link>
);
// handle gantt issue start date and target date
const handleUpdateDates = async (data: any) => {
const payload = {
id: data?.id,
start_date: data?.start_date,
target_date: data?.target_date,
};
console.log("payload", payload);
};
const blockFormat = (blocks: any) =>
blocks && blocks.length > 0
? blocks.map((_block: any) => {
let startDate = new Date(_block.created_at);
let targetDate = new Date(_block.updated_at);
let infoToggle = true;
if (_block?.start_date && _block.target_date) {
startDate = _block?.start_date;
targetDate = _block.target_date;
infoToggle = false;
}
return {
start_date: new Date(startDate),
target_date: new Date(targetDate),
infoToggle: infoToggle,
data: _block,
};
})
: [];
return (
<div className="w-full h-full p-3">
<GanttChartRoot
title="Cycles"
loaderTitle="Cycles"
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
blockUpdateHandler={handleUpdateDates}
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) =>
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
blockRender={(data: any) => <IssueGanttBlock issue={data as IIssue} />}
enableReorder={orderBy === "sort_order"}
/>
</div>
);

View file

@ -450,14 +450,14 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
height={12}
width={12}
className="rounded-full"
alt={cycle.owned_by.first_name}
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white">
{cycle.owned_by.first_name.charAt(0)}
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.first_name}</span>
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
</div>
</div>

View file

@ -250,14 +250,14 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.first_name}
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
{cycle.owned_by.first_name.charAt(0)}
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.first_name}</span>
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
</div>
</div>
<div className="flex h-5 items-center gap-2">

View file

@ -254,11 +254,11 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.first_name}
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
{cycle.owned_by.first_name.charAt(0)}
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
</div>

View file

@ -47,7 +47,7 @@ export const SingleEstimate: React.FC<Props> = ({
estimate: estimate.id,
};
mutateProjectDetails((prevData) => {
mutateProjectDetails((prevData: any) => {
if (!prevData) return prevData;
return { ...prevData, estimate: estimate.id };

View file

@ -0,0 +1,185 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import CSVIntegrationService from "services/integration/csv.services";
// hooks
import useToast from "hooks/use-toast";
// ui
import { SecondaryButton, PrimaryButton, CustomSearchSelect } from "components/ui";
// types
import { ICurrentUserResponse, IImporterService } from "types";
// fetch-keys
import useProjects from "hooks/use-projects";
type Props = {
isOpen: boolean;
handleClose: () => void;
data: IImporterService | null;
user: ICurrentUserResponse | undefined;
provider: string | string[];
mutateServices: () => void;
};
export const Exporter: React.FC<Props> = ({
isOpen,
handleClose,
user,
provider,
mutateServices,
}) => {
const [exportLoading, setExportLoading] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { projects } = useProjects();
const { setToastAlert } = useToast();
const options = projects?.map((project) => ({
value: project.id,
query: project.name + project.identifier,
content: (
<div className="flex items-center gap-2">
<span className="text-custom-text-200 text-[0.65rem]">{project.identifier}</span>
{project.name}
</div>
),
}));
const [value, setValue] = React.useState<string[]>([]);
const [multiple, setMultiple] = React.useState<boolean>(false);
const onChange = (val: any) => {
setValue(val);
};
const ExportCSVToMail = async () => {
setExportLoading(true);
if (workspaceSlug && user && typeof provider === "string") {
const payload = {
provider: provider,
project: value,
multiple: multiple,
};
await CSVIntegrationService.exportCSVService(workspaceSlug as string, payload, user)
.then(() => {
mutateServices();
router.push(`/${workspaceSlug}/settings/exports`);
setExportLoading(false);
setToastAlert({
type: "success",
title: "Export Successful",
message: `You will be able to download the exported ${
provider === "csv"
? "CSV"
: provider === "xlsx"
? "Excel"
: provider === "json"
? "JSON"
: ""
} from the previous export.`,
});
})
.catch(() => {
setExportLoading(false);
setToastAlert({
type: "error",
title: "Error!",
message: "Export was unsuccessful. Please try again.",
});
});
}
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 gap-y-4 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">
Export to{" "}
{provider === "csv"
? "CSV"
: provider === "xlsx"
? "Excel"
: provider === "json"
? "JSON"
: ""}
</h3>
</span>
</div>
<div>
<CustomSearchSelect
value={value ?? []}
onChange={(val: string[]) => onChange(val)}
options={options}
input={true}
label={
value && value.length > 0
? projects &&
projects
.filter((p) => value.includes(p.id))
.map((p) => p.identifier)
.join(", ")
: "All projects"
}
optionsClassName="min-w-full"
multiple
/>
</div>
<div
onClick={() => setMultiple(!multiple)}
className="flex items-center gap-2 max-w-min cursor-pointer"
>
<input
type="checkbox"
checked={multiple}
onChange={() => setMultiple(!multiple)}
/>
<div className="text-sm whitespace-nowrap">
Export the data into separate files
</div>
</div>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton
onClick={ExportCSVToMail}
disabled={exportLoading}
loading={exportLoading}
>
{exportLoading ? "Exporting..." : "Export"}
</PrimaryButton>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View file

@ -0,0 +1,171 @@
import { useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// hooks
import useUserAuth from "hooks/use-user-auth";
// services
import IntegrationService from "services/integration";
// components
import { Exporter, SingleExport } from "components/exporter";
// ui
import { Icon, Loader, PrimaryButton } from "components/ui";
// icons
import { ArrowPathIcon } from "@heroicons/react/24/outline";
// fetch-keys
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
// constants
import { EXPORTERS_LIST } from "constants/workspace";
const IntegrationGuide = () => {
const [refreshing, setRefreshing] = useState(false);
const per_page = 10;
const [cursor, setCursor] = useState<string | undefined>(`10:0:0`);
const router = useRouter();
const { workspaceSlug, provider } = router.query;
const { user } = useUserAuth();
const { data: exporterServices } = useSWR(
workspaceSlug && cursor
? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`)
: null,
workspaceSlug && cursor
? () => IntegrationService.getExportsServicesList(workspaceSlug as string, cursor, per_page)
: null
);
const handleCsvClose = () => {
router.replace(`/plane/settings/exports`);
};
return (
<>
<div className="h-full space-y-2">
<>
<div className="space-y-2">
{EXPORTERS_LIST.map((service) => (
<div
key={service.provider}
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"
>
<div className="flex items-center gap-4 whitespace-nowrap">
<div className="relative h-10 w-10 flex-shrink-0">
<Image
src={service.logo}
layout="fill"
objectFit="cover"
alt={`${service.title} Logo`}
/>
</div>
<div className="w-full">
<h3>{service.title}</h3>
<p className="text-sm text-custom-text-200">{service.description}</p>
</div>
<div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}>
<a>
<PrimaryButton>
<span className="capitalize">{service.type}</span> now
</PrimaryButton>
</a>
</Link>
</div>
</div>
</div>
))}
</div>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4">
<h3 className="mb-2 flex gap-2 text-lg font-medium justify-between">
<div className="flex gap-2">
<div className="">Previous Exports</div>
<button
type="button"
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
onClick={() => {
setRefreshing(true);
mutate(
EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)
).then(() => setRefreshing(false));
}}
>
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
{refreshing ? "Refreshing..." : "Refresh status"}
</button>
</div>
<div className="flex gap-2 items-center text-xs">
<button
disabled={!exporterServices?.prev_page_results}
onClick={() =>
exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)
}
className={`flex items-center border border-custom-primary-100 text-custom-primary-100 px-1 rounded ${
exporterServices?.prev_page_results
? "cursor-pointer hover:bg-custom-primary-100 hover:text-white"
: "cursor-not-allowed opacity-75"
}`}
>
<Icon iconName="keyboard_arrow_left" className="!text-lg" />
<div className="pr-1">Prev</div>
</button>
<button
disabled={!exporterServices?.next_page_results}
onClick={() =>
exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)
}
className={`flex items-center border border-custom-primary-100 text-custom-primary-100 px-1 rounded ${
exporterServices?.next_page_results
? "cursor-pointer hover:bg-custom-primary-100 hover:text-white"
: "cursor-not-allowed opacity-75"
}`}
>
<div className="pl-1">Next</div>
<Icon iconName="keyboard_arrow_right" className="!text-lg" />
</button>
</div>
</h3>
{exporterServices && exporterServices?.results ? (
exporterServices?.results?.length > 0 ? (
<div className="space-y-2">
<div className="divide-y divide-custom-border-200">
{exporterServices?.results.map((service) => (
<SingleExport key={service.id} service={service} refreshing={refreshing} />
))}
</div>
</div>
) : (
<p className="py-2 text-sm text-custom-text-200">No previous export available.</p>
)
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
)}
</div>
</>
{provider && (
<Exporter
isOpen={true}
handleClose={() => handleCsvClose()}
data={null}
user={user}
provider={provider}
mutateServices={() =>
mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))
}
/>
)}
</div>
</>
);
};
export default IntegrationGuide;

View file

@ -0,0 +1,4 @@
//layout
export * from "./single-export";
// csv
export * from "./export-modal";

View file

@ -0,0 +1,81 @@
import React from "react";
// next imports
import Link from "next/link";
// ui
import { PrimaryButton } from "components/ui"; // icons
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IExportData } from "types";
type Props = {
service: IExportData;
refreshing: boolean;
};
export const SingleExport: React.FC<Props> = ({ service, refreshing }) => {
const provider = service.provider;
const [isLoading, setIsLoading] = React.useState(false);
const checkExpiry = (inputDateString: string) => {
const currentDate = new Date();
const expiryDate = new Date(inputDateString);
expiryDate.setDate(expiryDate.getDate() + 7);
return expiryDate > currentDate;
};
return (
<div className="flex items-center justify-between gap-2 py-3">
<div>
<h4 className="flex items-center gap-2 text-sm">
<span>
Export to{" "}
<span className="font-medium">
{provider === "csv"
? "CSV"
: provider === "xlsx"
? "Excel"
: provider === "json"
? "JSON"
: ""}
</span>{" "}
</span>
<span
className={`rounded px-2 py-0.5 text-xs capitalize ${
service.status === "completed"
? "bg-green-500/20 text-green-500"
: service.status === "processing"
? "bg-yellow-500/20 text-yellow-500"
: service.status === "failed"
? "bg-red-500/20 text-red-500"
: service.status === "expired"
? "bg-orange-500/20 text-orange-500"
: ""
}`}
>
{refreshing ? "Refreshing..." : service.status}
</span>
</h4>
<div className="mt-2 flex items-center gap-2 text-xs text-custom-text-200">
<span>{renderShortDateWithYearFormat(service.created_at)}</span>|
<span>Exported by {service?.initiated_by_detail?.display_name}</span>
</div>
</div>
{checkExpiry(service.created_at) ? (
<>
{service.status == "completed" && (
<div>
<Link href={service?.url}>
<PrimaryButton className="w-full text-center">
{isLoading ? "Downloading..." : "Download"}
</PrimaryButton>
</Link>
</div>
)}
</>
) : (
<div className="text-xs text-red-500">Expired</div>
)}
</div>
);
};

View file

@ -0,0 +1,103 @@
import Link from "next/link";
import { useRouter } from "next/router";
// ui
import { Tooltip } from "components/ui";
// helpers
import { renderShortDate } from "helpers/date-time.helper";
// types
import { ICycle, IIssue, IModule } from "types";
// constants
import { MODULE_STATUS } from "constants/module";
export const IssueGanttBlock = ({ issue }: { issue: IIssue }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
<div
className="flex-shrink-0 w-0.5 h-full"
style={{ backgroundColor: issue.state_detail?.color }}
/>
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{issue.name}</h5>
<div>
{renderShortDate(issue.start_date ?? "")} to{" "}
{renderShortDate(issue.target_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
{issue.name}
</div>
</Tooltip>
</a>
</Link>
);
};
export const CycleGanttBlock = ({ cycle }: { cycle: ICycle }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Link href={`/${workspaceSlug}/projects/${cycle.project}/cycles/${cycle.id}`}>
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
<div className="flex-shrink-0 w-0.5 h-full bg-custom-primary-100" />
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{cycle.name}</h5>
<div>
{renderShortDate(cycle.start_date ?? "")} to {renderShortDate(cycle.end_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
{cycle.name}
</div>
</Tooltip>
</a>
</Link>
);
};
export const ModuleGanttBlock = ({ module }: { module: IModule }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
<div
className="flex-shrink-0 w-0.5 h-full"
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === module.status)?.color }}
/>
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{module.name}</h5>
<div>
{renderShortDate(module.start_date ?? "")} to{" "}
{renderShortDate(module.target_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
{module.name}
</div>
</Tooltip>
</a>
</Link>
);
};

View file

@ -0,0 +1,178 @@
import { FC } from "react";
// react-beautiful-dnd
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// helpers
import { ChartDraggable } from "../helpers/draggable";
import { renderDateFormat } from "helpers/date-time.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
export const GanttChartBlocks: FC<{
itemsContainerWidth: number;
blocks: IGanttBlock[] | null;
sidebarBlockRender: FC;
blockRender: FC;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableLeftDrag: boolean;
enableRightDrag: boolean;
enableReorder: boolean;
}> = ({
itemsContainerWidth,
blocks,
sidebarBlockRender,
blockRender,
blockUpdateHandler,
enableLeftDrag,
enableRightDrag,
enableReorder,
}) => {
const handleChartBlockPosition = (
block: IGanttBlock,
totalBlockShifts: number,
dragDirection: "left" | "right"
) => {
let updatedDate = new Date();
if (dragDirection === "left") {
const originalDate = new Date(block.start_date);
const currentDay = originalDate.getDate();
updatedDate = new Date(originalDate);
updatedDate.setDate(currentDay - totalBlockShifts);
} else {
const originalDate = new Date(block.target_date);
const currentDay = originalDate.getDate();
updatedDate = new Date(originalDate);
updatedDate.setDate(currentDay + totalBlockShifts);
}
blockUpdateHandler(block.data, {
[dragDirection === "left" ? "start_date" : "target_date"]: renderDateFormat(updatedDate),
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination, draggableId } = result;
if (!destination) return;
if (source.index === destination.index && document) {
// const draggedBlock = document.querySelector(`#${draggableId}`) as HTMLElement;
// const blockStyles = window.getComputedStyle(draggedBlock);
// console.log(blockStyles.marginLeft);
return;
}
let updatedSortOrder = blocks[source.index].sort_order;
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
else if (destination.index === blocks.length - 1)
updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<div
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
style={{ width: `${itemsContainerWidth}px` }}
>
<DragDropContext onDragEnd={handleOrderChange}>
<StrictModeDroppable droppableId="gantt">
{(droppableProvided, droppableSnapshot) => (
<div
className="w-full space-y-2"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<>
{blocks &&
blocks.length > 0 &&
blocks.map(
(block, index: number) =>
block.start_date &&
block.target_date && (
<Draggable
key={`block-${block.id}`}
draggableId={`block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided) => (
<div
className={
droppableSnapshot.isDraggingOver ? "bg-custom-border-100/10" : ""
}
ref={provided.innerRef}
{...provided.draggableProps}
>
<ChartDraggable
block={block}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableLeftDrag={enableLeftDrag}
enableRightDrag={enableRightDrag}
provided={provided}
>
<div
className="rounded shadow-sm bg-custom-background-80 overflow-hidden h-9 flex items-center transition-all"
style={{
width: `${block.position?.width}px`,
}}
>
{blockRender({
...block.data,
})}
</div>
</ChartDraggable>
</div>
)}
</Draggable>
)
)}
{droppableProvided.placeholder}
</>
</div>
)}
</StrictModeDroppable>
</DragDropContext>
{/* sidebar */}
{/* <div className="fixed top-0 bottom-0 w-[300px] flex-shrink-0 divide-y divide-custom-border-200 border-r border-custom-border-200 overflow-y-auto">
{blocks &&
blocks.length > 0 &&
blocks.map((block: any, _idx: number) => (
<div className="relative h-[40px] bg-custom-background-100" key={`sidebar-blocks-${_idx}`}>
{sidebarBlockRender(block?.data)}
</div>
))}
</div> */}
</div>
);
};

View file

@ -0,0 +1,2 @@
export * from "./block";
export * from "./blocks-display";

View file

@ -1,82 +0,0 @@
import { FC, useEffect, useState } from "react";
// helpers
import { ChartDraggable } from "../helpers/draggable";
// data
import { datePreview } from "../data";
export const GanttChartBlocks: FC<{
itemsContainerWidth: number;
blocks: null | any[];
sidebarBlockRender: FC;
blockRender: FC;
}> = ({ itemsContainerWidth, blocks, sidebarBlockRender, blockRender }) => {
const handleChartBlockPosition = (block: any) => {
// setChartBlocks((prevData: any) =>
// prevData.map((_block: any) => (_block?.data?.id == block?.data?.id ? block : _block))
// );
};
return (
<div
className="relative z-[5] mt-[58px] h-full w-[4000px] divide-x divide-gray-300 overflow-hidden overflow-y-auto"
style={{ width: `${itemsContainerWidth}px` }}
>
<div className="w-full">
{blocks &&
blocks.length > 0 &&
blocks.map((block: any, _idx: number) => (
<>
{block.start_date && block.target_date && (
<ChartDraggable
className="relative flex h-[40px] items-center"
key={`blocks-${_idx}`}
block={block}
handleBlock={handleChartBlockPosition}
>
<div
className="relative group inline-flex cursor-pointer items-center font-medium transition-all"
style={{ marginLeft: `${block?.position?.marginLeft}px` }}
>
<div className="flex-shrink-0 relative w-0 h-0 flex items-center invisible group-hover:visible whitespace-nowrap">
<div className="absolute right-0 mr-[5px] rounded-sm bg-custom-background-90 px-2 py-0.5 text-xs font-medium">
{block?.start_date ? datePreview(block?.start_date) : "-"}
</div>
</div>
<div
className="rounded shadow-sm bg-custom-background-100 overflow-hidden relative flex items-center h-[34px] border border-custom-border-200"
style={{
width: `${block?.position?.width}px`,
}}
>
{blockRender({
...block?.data,
infoToggle: block?.infoToggle ? true : false,
})}
</div>
<div className="flex-shrink-0 relative w-0 h-0 flex items-center invisible group-hover:visible whitespace-nowrap">
<div className="absolute left-0 ml-[5px] mr-[5px] rounded-sm bg-custom-background-90 px-2 py-0.5 text-xs font-medium">
{block?.target_date ? datePreview(block?.target_date) : "-"}
</div>
</div>
</div>
</ChartDraggable>
)}
</>
))}
</div>
{/* sidebar */}
{/* <div className="fixed top-0 bottom-0 w-[300px] flex-shrink-0 divide-y divide-custom-border-200 border-r border-custom-border-200 overflow-y-auto">
{blocks &&
blocks.length > 0 &&
blocks.map((block: any, _idx: number) => (
<div className="relative h-[40px] bg-custom-background-100" key={`sidebar-blocks-${_idx}`}>
{sidebarBlockRender(block?.data)}
</div>
))}
</div> */}
</div>
);
};

View file

@ -25,7 +25,7 @@ export const BiWeekChartView: FC<any> = () => {
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${

View file

@ -25,7 +25,7 @@ export const DayChartView: FC<any> = () => {
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${

View file

@ -25,7 +25,7 @@ export const HourChartView: FC<any> = () => {
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${

View file

@ -1,13 +1,8 @@
import { FC, useEffect, useState } from "react";
// icons
import {
Bars4Icon,
XMarkIcon,
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
} from "@heroicons/react/20/solid";
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
// components
import { GanttChartBlocks } from "../blocks";
import { GanttChartBlocks } from "components/gantt-chart";
// import { HourChartView } from "./hours";
// import { DayChartView } from "./day";
// import { WeekChartView } from "./week";
@ -30,9 +25,9 @@ import {
getMonthChartItemPositionWidthInMonth,
} from "../views";
// types
import { ChartDataType } from "../types";
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
// data
import { datePreview, currentViewDataWithView } from "../data";
import { currentViewDataWithView } from "../data";
// context
import { useChart } from "../hooks";
@ -40,10 +35,13 @@ type ChartViewRootProps = {
border: boolean;
title: null | string;
loaderTitle: string;
blocks: any;
blockUpdateHandler: (data: any) => void;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
sidebarBlockRender: FC<any>;
blockRender: FC<any>;
enableLeftDrag: boolean;
enableRightDrag: boolean;
enableReorder: boolean;
};
export const ChartViewRoot: FC<ChartViewRootProps> = ({
@ -54,6 +52,9 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
blockUpdateHandler,
sidebarBlockRender,
blockRender,
enableLeftDrag,
enableRightDrag,
enableReorder,
}) => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
@ -62,13 +63,13 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
const [blocksSidebarView, setBlocksSidebarView] = useState<boolean>(false);
// blocks state management starts
const [chartBlocks, setChartBlocks] = useState<any[] | null>(null);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
const renderBlockStructure = (view: any, blocks: any) =>
const renderBlockStructure = (view: any, blocks: IGanttBlock[]) =>
blocks && blocks.length > 0
? blocks.map((_block: any) => ({
..._block,
position: getMonthChartItemPositionWidthInMonth(view, _block),
? blocks.map((block: any) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
: [];
@ -154,13 +155,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
const updatingCurrentLeftScrollPosition = (width: number) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
scrollContainer.scrollLeft = width + scrollContainer.scrollLeft;
setItemsContainerWidth(width + scrollContainer.scrollLeft);
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
};
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
const clientVisibleWidth: number = scrollContainer.clientWidth;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
let scrollWidth: number = 0;
let daysDifference: number = 0;
@ -189,9 +191,9 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
const onScroll = () => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
const scrollWidth: number = scrollContainer.scrollWidth;
const clientVisibleWidth: number = scrollContainer.clientWidth;
const currentScrollPosition: number = scrollContainer.scrollLeft;
const scrollWidth: number = scrollContainer?.scrollWidth;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
const currentScrollPosition: number = scrollContainer?.scrollLeft;
const approxRangeLeft: number =
scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
@ -207,6 +209,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
scrollContainer.addEventListener("scroll", onScroll);
return () => {
scrollContainer.removeEventListener("scroll", onScroll);
};
@ -242,7 +245,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
</div> */}
{/* chart header */}
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-5 gap-y-3 whitespace-nowrap p-2">
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap p-2">
{/* <div
className="transition-all border border-custom-border-200 w-[30px] h-[30px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
onClick={() => setBlocksSidebarView(() => !blocksSidebarView)}
@ -301,8 +304,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
</div>
<div
className="transition-all border border-custom-border-200 w-[30px] h-[30px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
onClick={() => setFullScreenMode(() => !fullScreenMode)}
className="transition-all border border-custom-border-200 p-1 flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
onClick={() => setFullScreenMode((prevData) => !prevData)}
>
{fullScreenMode ? (
<ArrowsPointingInIcon className="h-4 w-4" />
@ -325,6 +328,10 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
blocks={chartBlocks}
sidebarBlockRender={sidebarBlockRender}
blockRender={blockRender}
blockUpdateHandler={blockUpdateHandler}
enableLeftDrag={enableLeftDrag}
enableRightDrag={enableRightDrag}
enableReorder={enableReorder}
/>
)}

View file

@ -1,48 +1,55 @@
import { FC } from "react";
// context
// hooks
import { useChart } from "../hooks";
// types
import { IMonthBlock } from "../views";
export const MonthChartView: FC<any> = () => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
const { currentViewData, renderView } = useChart();
const monthBlocks: IMonthBlock[] = renderView;
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-200">
{renderView &&
renderView.length > 0 &&
renderView.map((_itemRoot: any, _idxRoot: any) => (
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100/50">
{monthBlocks &&
monthBlocks.length > 0 &&
monthBlocks.map((block, _idxRoot) => (
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
<div className="relative border-b border-custom-border-200">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{_itemRoot?.title}
{block?.title}
</div>
</div>
<div className="flex h-full w-full divide-x divide-custom-border-200">
{_itemRoot.children &&
_itemRoot.children.length > 0 &&
_itemRoot.children.map((_item: any, _idx: any) => (
<div className="flex h-full w-full divide-x divide-custom-border-100/50">
{block?.children &&
block?.children.length > 0 &&
block?.children.map((monthDay, _idx) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
_item?.today ? `text-red-500 border-red-500` : `border-custom-border-200`
monthDay?.today
? `text-red-500 border-red-500`
: `border-custom-border-200`
}`}
>
<div>{_item.title}</div>
<div>{monthDay?.title}</div>
</div>
<div
className={`relative h-full w-full flex-1 flex justify-center ${
["sat", "sun"].includes(_item?.dayData?.shortTitle || "")
["sat", "sun"].includes(monthDay?.dayData?.shortTitle || "")
? `bg-custom-background-90`
: ``
}`}
>
{_item?.today && (
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
{monthDay?.today && (
<div className="absolute top-0 bottom-0 w-[1px] bg-red-500" />
)}
</div>
</div>

View file

@ -25,7 +25,7 @@ export const QuarterChartView: FC<any> = () => {
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${

View file

@ -25,7 +25,7 @@ export const WeekChartView: FC<any> = () => {
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${

View file

@ -25,7 +25,7 @@ export const YearChartView: FC<any> = () => {
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${

View file

@ -0,0 +1,14 @@
// types
import { IIssue } from "types";
import { IGanttBlock } from "components/gantt-chart";
export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] =>
blocks && blocks.length > 0
? blocks.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: new Date(block.start_date ?? ""),
target_date: new Date(block.target_date ?? ""),
}))
: [];

View file

@ -1,138 +1,205 @@
import { useState, useRef } from "react";
import React, { useRef, useState } from "react";
export const ChartDraggable = ({ children, block, handleBlock, className }: any) => {
const [dragging, setDragging] = useState(false);
// react-beautiful-dnd
import { DraggableProvided } from "react-beautiful-dnd";
import { useChart } from "../hooks";
// types
import { IGanttBlock } from "../types";
const [chartBlockPositionLeft, setChartBlockPositionLeft] = useState(0);
const [blockPositionLeft, setBlockPositionLeft] = useState(0);
const [dragBlockOffsetX, setDragBlockOffsetX] = useState(0);
type Props = {
children: any;
block: IGanttBlock;
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right") => void;
enableLeftDrag: boolean;
enableRightDrag: boolean;
provided: DraggableProvided;
};
const handleMouseDown = (event: any) => {
const chartBlockPositionLeft: number = block.position.marginLeft;
const blockPositionLeft: number = event.target.getBoundingClientRect().left;
const dragBlockOffsetX: number = event.clientX - event.target.getBoundingClientRect().left;
export const ChartDraggable: React.FC<Props> = ({
children,
block,
handleBlock,
enableLeftDrag = true,
enableRightDrag = true,
provided,
}) => {
const [isLeftResizing, setIsLeftResizing] = useState(false);
const [isRightResizing, setIsRightResizing] = useState(false);
console.log("--------------------");
console.log("chartBlockPositionLeft", chartBlockPositionLeft);
console.log("blockPositionLeft", blockPositionLeft);
console.log("dragBlockOffsetX", dragBlockOffsetX);
console.log("-->");
const parentDivRef = useRef<HTMLDivElement>(null);
const resizableRef = useRef<HTMLDivElement>(null);
setDragging(true);
setChartBlockPositionLeft(chartBlockPositionLeft);
setBlockPositionLeft(blockPositionLeft);
setDragBlockOffsetX(dragBlockOffsetX);
const { currentViewData } = useChart();
const checkScrollEnd = (e: MouseEvent): number => {
let delWidth = 0;
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
const appSidebar = document.querySelector("#app-sidebar") as HTMLElement;
const posFromLeft = e.clientX;
// manually scroll to left if reached the left end while dragging
if (posFromLeft - appSidebar.clientWidth <= 70) {
if (e.movementX > 0) return 0;
delWidth = -5;
scrollContainer.scrollBy(delWidth, 0);
} else delWidth = e.movementX;
// manually scroll to right if reached the right end while dragging
const posFromRight = window.innerWidth - e.clientX;
if (posFromRight <= 70) {
if (e.movementX < 0) return 0;
delWidth = 5;
scrollContainer.scrollBy(delWidth, 0);
} else delWidth = e.movementX;
return delWidth;
};
const handleMouseMove = (event: any) => {
if (!dragging) return;
const handleLeftDrag = () => {
if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position)
return;
const currentBlockPosition = event.clientX - dragBlockOffsetX;
console.log("currentBlockPosition", currentBlockPosition);
if (currentBlockPosition <= blockPositionLeft) {
const updatedPosition = chartBlockPositionLeft - (blockPositionLeft - currentBlockPosition);
console.log("updatedPosition", updatedPosition);
handleBlock({ ...block, position: { ...block.position, marginLeft: updatedPosition } });
} else {
const updatedPosition = chartBlockPositionLeft + (blockPositionLeft - currentBlockPosition);
console.log("updatedPosition", updatedPosition);
handleBlock({ ...block, position: { ...block.position, marginLeft: updatedPosition } });
}
console.log("--------------------");
const resizableDiv = resizableRef.current;
const parentDiv = parentDivRef.current;
const columnWidth = currentViewData.data.width;
const blockInitialWidth =
resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
let initialMarginLeft = parseInt(parentDiv.style.marginLeft);
const handleMouseMove = (e: MouseEvent) => {
if (!window) return;
let delWidth = 0;
delWidth = checkScrollEnd(e);
// calculate new width and update the initialMarginLeft using -=
const newWidth = Math.round((initialWidth -= delWidth) / columnWidth) * columnWidth;
// calculate new marginLeft and update the initial marginLeft to the newly calculated one
const newMarginLeft = initialMarginLeft - (newWidth - (block.position?.width ?? 0));
initialMarginLeft = newMarginLeft;
// block needs to be at least 1 column wide
if (newWidth < columnWidth) return;
resizableDiv.style.width = `${newWidth}px`;
parentDiv.style.marginLeft = `${newMarginLeft}px`;
if (block.position) {
block.position.width = newWidth;
block.position.marginLeft = newMarginLeft;
}
};
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
const totalBlockShifts = Math.ceil(
(resizableDiv.clientWidth - blockInitialWidth) / columnWidth
);
handleBlock(totalBlockShifts, "left");
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
const handleMouseUp = () => {
setDragging(false);
setChartBlockPositionLeft(0);
setBlockPositionLeft(0);
setDragBlockOffsetX(0);
const handleRightDrag = () => {
if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position)
return;
const resizableDiv = resizableRef.current;
const columnWidth = currentViewData.data.width;
const blockInitialWidth =
resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
const handleMouseMove = (e: MouseEvent) => {
if (!window) return;
let delWidth = 0;
delWidth = checkScrollEnd(e);
// calculate new width and update the initialMarginLeft using +=
const newWidth = Math.round((initialWidth += delWidth) / columnWidth) * columnWidth;
// block needs to be at least 1 column wide
if (newWidth < columnWidth) return;
resizableDiv.style.width = `${Math.max(newWidth, 80)}px`;
if (block.position) block.position.width = Math.max(newWidth, 80);
};
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
const totalBlockShifts = Math.ceil(
(resizableDiv.clientWidth - blockInitialWidth) / columnWidth
);
handleBlock(totalBlockShifts, "right");
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
return (
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
className={`${className ? className : ``}`}
id={`block-${block.id}`}
ref={parentDivRef}
className="relative group inline-flex cursor-pointer items-center font-medium transition-all"
style={{
marginLeft: `${block.position?.marginLeft}px`,
}}
>
{children}
{enableLeftDrag && (
<>
<div
onMouseDown={handleLeftDrag}
onMouseEnter={() => setIsLeftResizing(true)}
onMouseLeave={() => setIsLeftResizing(false)}
className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[1] w-6 h-10 bg-brand-backdrop rounded-md cursor-col-resize"
/>
<div
className={`absolute top-1/2 -translate-y-1/2 w-1 h-4/5 rounded-sm bg-custom-background-80 transition-all duration-300 ${
isLeftResizing ? "-left-2.5" : "left-1"
}`}
/>
</>
)}
{React.cloneElement(children, { ref: resizableRef, ...provided.dragHandleProps })}
{enableRightDrag && (
<>
<div
onMouseDown={handleRightDrag}
onMouseEnter={() => setIsRightResizing(true)}
onMouseLeave={() => setIsRightResizing(false)}
className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[1] w-6 h-6 bg-brand-backdrop rounded-md cursor-col-resize"
/>
<div
className={`absolute top-1/2 -translate-y-1/2 w-1 h-4/5 rounded-sm bg-custom-background-80 transition-all duration-300 ${
isRightResizing ? "-right-2.5" : "right-1"
}`}
/>
</>
)}
</div>
);
};
// import { useState } from "react";
// export const ChartDraggable = ({ children, id, className = "", style }: any) => {
// const [dragging, setDragging] = useState(false);
// const [chartBlockPositionLeft, setChartBlockPositionLeft] = useState(0);
// const [blockPositionLeft, setBlockPositionLeft] = useState(0);
// const [dragBlockOffsetX, setDragBlockOffsetX] = useState(0);
// const handleDragStart = (event: any) => {
// // event.dataTransfer.setData("text/plain", event.target.id);
// const chartBlockPositionLeft: number = parseInt(event.target.style.left.slice(0, -2));
// const blockPositionLeft: number = event.target.getBoundingClientRect().left;
// const dragBlockOffsetX: number = event.clientX - event.target.getBoundingClientRect().left;
// console.log("chartBlockPositionLeft", chartBlockPositionLeft);
// console.log("blockPositionLeft", blockPositionLeft);
// console.log("dragBlockOffsetX", dragBlockOffsetX);
// console.log("--------------------");
// setDragging(true);
// setChartBlockPositionLeft(chartBlockPositionLeft);
// setBlockPositionLeft(blockPositionLeft);
// setDragBlockOffsetX(dragBlockOffsetX);
// };
// const handleDragEnd = () => {
// setDragging(false);
// setChartBlockPositionLeft(0);
// setBlockPositionLeft(0);
// setDragBlockOffsetX(0);
// };
// const handleDragOver = (event: any) => {
// event.preventDefault();
// if (dragging) {
// const scrollContainer = document.getElementById(`block-parent-${id}`) as HTMLElement;
// const currentBlockPosition = event.clientX - dragBlockOffsetX;
// console.log('currentBlockPosition')
// if (currentBlockPosition <= blockPositionLeft) {
// const updatedPosition = chartBlockPositionLeft - (blockPositionLeft - currentBlockPosition);
// console.log("updatedPosition", updatedPosition);
// if (scrollContainer) scrollContainer.style.left = `${updatedPosition}px`;
// } else {
// const updatedPosition = chartBlockPositionLeft + (blockPositionLeft - currentBlockPosition);
// console.log("updatedPosition", updatedPosition);
// if (scrollContainer) scrollContainer.style.left = `${updatedPosition}px`;
// }
// console.log("--------------------");
// }
// };
// const handleDrop = (event: any) => {
// event.preventDefault();
// setDragging(false);
// setChartBlockPositionLeft(0);
// setBlockPositionLeft(0);
// setDragBlockOffsetX(0);
// };
// return (
// <div
// id={id}
// draggable
// onDragStart={handleDragStart}
// onDragEnd={handleDragEnd}
// onDragOver={handleDragOver}
// onDrop={handleDrop}
// className={`${className} ${dragging ? "dragging" : ""}`}
// style={style}
// >
// {children}
// </div>
// );
// };

View file

@ -0,0 +1 @@
export * from "./block-structure";

View file

@ -0,0 +1,41 @@
import { KeyedMutator } from "swr";
// services
import issuesService from "services/issues.service";
// types
import { ICurrentUserResponse, IIssue } from "types";
import { IBlockUpdateData } from "../types";
export const updateGanttIssue = (
issue: IIssue,
payload: IBlockUpdateData,
mutate: KeyedMutator<any>,
user: ICurrentUserResponse | undefined,
workspaceSlug: string | undefined
) => {
if (!issue || !workspaceSlug || !user) return;
mutate((prevData: any) => {
if (!prevData) return prevData;
const newList = prevData.map((p: any) => ({
...p,
...(p.id === issue.id ? payload : {}),
}));
if (payload.sort_order) {
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
removedElement.sort_order = payload.sort_order.newSortOrder;
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
}
return newList;
}, false);
const newPayload: any = { ...payload };
if (newPayload.sort_order && payload.sort_order)
newPayload.sort_order = payload.sort_order.newSortOrder;
issuesService.patchIssue(workspaceSlug, issue.project, issue.id, newPayload, user);
};

View file

@ -1 +1,5 @@
export * from "./blocks";
export * from "./helpers";
export * from "./hooks";
export * from "./root";
export * from "./types";

View file

@ -3,15 +3,20 @@ import { FC } from "react";
import { ChartViewRoot } from "./chart";
// context
import { ChartContextProvider } from "./contexts";
// types
import { IBlockUpdateData, IGanttBlock } from "./types";
type GanttChartRootProps = {
border?: boolean;
title: null | string;
loaderTitle: string;
blocks: any;
blockUpdateHandler: (data: any) => void;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
sidebarBlockRender: FC<any>;
blockRender: FC<any>;
enableLeftDrag?: boolean;
enableRightDrag?: boolean;
enableReorder?: boolean;
};
export const GanttChartRoot: FC<GanttChartRootProps> = ({
@ -22,6 +27,9 @@ export const GanttChartRoot: FC<GanttChartRootProps> = ({
blockUpdateHandler,
sidebarBlockRender,
blockRender,
enableLeftDrag = true,
enableRightDrag = true,
enableReorder = true,
}) => (
<ChartContextProvider>
<ChartViewRoot
@ -32,6 +40,9 @@ export const GanttChartRoot: FC<GanttChartRootProps> = ({
blockUpdateHandler={blockUpdateHandler}
sidebarBlockRender={sidebarBlockRender}
blockRender={blockRender}
enableLeftDrag={enableLeftDrag}
enableRightDrag={enableRightDrag}
enableReorder={enableReorder}
/>
</ChartContextProvider>
);

View file

@ -5,10 +5,32 @@ export type allViewsType = {
data: Object | null;
};
export interface IGanttBlock {
data: any;
id: string;
position?: {
marginLeft: number;
width: number;
};
sort_order: number;
start_date: Date;
target_date: Date;
}
export interface IBlockUpdateData {
sort_order?: {
destinationIndex: number;
newSortOrder: number;
sourceIndex: number;
};
start_date?: string;
target_date?: string;
}
export interface ChartContextData {
allViews: allViewsType[];
currentView: "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year";
currentViewData: any;
currentViewData: ChartDataType | undefined;
renderView: any;
}

View file

@ -1,5 +1,5 @@
// types
import { ChartDataType } from "../types";
import { ChartDataType, IGanttBlock } from "../types";
// data
import { weeks, months } from "../data";
// helpers
@ -19,7 +19,35 @@ type GetAllDaysInMonthInMonthViewType = {
active: boolean;
today: boolean;
};
const getAllDaysInMonthInMonthView = (month: number, year: number) => {
interface IMonthChild {
active: boolean;
date: Date;
day: number;
dayData: {
key: number;
shortTitle: string;
title: string;
};
title: string;
today: boolean;
weekNumber: number;
}
export interface IMonthBlock {
children: IMonthChild[];
month: number;
monthData: {
key: number;
shortTitle: string;
title: string;
};
title: string;
year: number;
}
[];
const getAllDaysInMonthInMonthView = (month: number, year: number): IMonthChild[] => {
const day: GetAllDaysInMonthInMonthViewType[] = [];
const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year);
const currentDate = new Date();
@ -45,7 +73,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number) => {
return day;
};
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => {
const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number): IMonthBlock => {
const currentMonth: number = month;
const currentYear: number = year;
@ -162,7 +190,11 @@ export const getNumberOfDaysBetweenTwoDatesInMonth = (startDate: Date, endDate:
return daysDifference;
};
export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, itemData: any) => {
// calc item scroll position and width
export const getMonthChartItemPositionWidthInMonth = (
chartData: ChartDataType,
itemData: IGanttBlock
) => {
let scrollPosition: number = 0;
let scrollWidth: number = 0;

View file

@ -100,8 +100,6 @@ export const generateYearChart = (yearPayload: ChartDataType, side: null | "left
.map((monthData: any) => monthData.children.length)
.reduce((partialSum: number, a: number) => partialSum + a, 0) * yearPayload.data.width;
console.log("scrollWidth", scrollWidth);
return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth };
};

View file

@ -72,8 +72,8 @@ export const InboxActionHeader = () => {
false
);
mutateInboxIssues(
(prevData) =>
(prevData ?? []).map((i) =>
(prevData: any) =>
(prevData ?? []).map((i: any) =>
i.bridge_id === inboxIssueId
? { ...i, issue_inbox: [{ ...i.issue_inbox[0], ...data }] }
: i

View file

@ -163,7 +163,7 @@ export const GithubImporterRoot: React.FC<Props> = ({ user }) => {
await GithubIntegrationService.createGithubServiceImport(workspaceSlug as string, payload, user)
.then(() => {
router.push(`/${workspaceSlug}/settings/import-export`);
router.push(`/${workspaceSlug}/settings/imports`);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string));
})
.catch(() =>
@ -178,7 +178,7 @@ export const GithubImporterRoot: React.FC<Props> = ({ user }) => {
return (
<form onSubmit={handleSubmit(createGithubImporterService)}>
<div className="space-y-2">
<Link href={`/${workspaceSlug}/settings/import-export`}>
<Link href={`/${workspaceSlug}/settings/imports`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-custom-text-200 hover:text-custom-text-100">
<ArrowLeftIcon className="h-3 w-3" />
<div>Cancel import & go back</div>

View file

@ -44,19 +44,12 @@ export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users,
);
const options = members?.map((member) => ({
value: member.member.email,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
value: member.member.display_name,
query: member.member.display_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name + "(" + member.member.email + ")"
: member.member.email}
{member.member.display_name}
</div>
),
}));

View file

@ -58,7 +58,7 @@ const IntegrationGuide = () => {
user={user}
/>
<div className="h-full space-y-2">
{!provider && (
{(!provider || provider === "csv") && (
<>
<div className="mb-5 flex items-center gap-2">
<div className="h-full w-full space-y-1">
@ -100,7 +100,7 @@ const IntegrationGuide = () => {
</div>
<div className="flex-shrink-0">
<Link
href={`/${workspaceSlug}/settings/import-export?provider=${service.provider}`}
href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}
>
<a>
<PrimaryButton>

View file

@ -3,11 +3,17 @@ import { FC } from "react";
// next
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// react-hook-form
import { useFormContext, useFieldArray, Controller } from "react-hook-form";
// hooks
import useWorkspaceMembers from "hooks/use-workspace-members";
// fetch keys
import { WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys";
// services
import workspaceService from "services/workspace.service";
// components
import { ToggleSwitch, Input, CustomSelect, CustomSearchSelect, Avatar } from "components/ui";
@ -30,22 +36,20 @@ export const JiraImportUsers: FC = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceMembers: members } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
const { data: members } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug?.toString() ?? "") : null,
workspaceSlug
? () => workspaceService.workspaceMembersWithEmail(workspaceSlug?.toString() ?? "")
: null
);
const options = members?.map((member) => ({
value: member.member.email,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
query: member.member.display_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name + " (" + member.member.email + ")"
: member.member.email}
{member.member.display_name}
</div>
),
}));

View file

@ -92,7 +92,7 @@ export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
.createJiraImporter(workspaceSlug.toString(), data, user)
.then(() => {
mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString()));
router.push(`/${workspaceSlug}/settings/import-export`);
router.push(`/${workspaceSlug}/settings/imports`);
})
.catch((err) => {
console.log(err);
@ -109,7 +109,7 @@ export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
return (
<div className="flex h-full flex-col space-y-2">
<Link href={`/${workspaceSlug}/settings/import-export`}>
<Link href={`/${workspaceSlug}/settings/imports`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-custom-text-200 hover:text-custom-text-100">
<div>
<ArrowLeftIcon className="h-3 w-3" />

View file

@ -42,12 +42,7 @@ export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelet
</h4>
<div className="mt-2 flex items-center gap-2 text-xs text-custom-text-200">
<span>{renderShortDateWithYearFormat(service.created_at)}</span>|
<span>
Imported by{" "}
{service.initiated_by_detail.first_name && service.initiated_by_detail.first_name !== ""
? service.initiated_by_detail.first_name + " " + service.initiated_by_detail.last_name
: service.initiated_by_detail.email}
</span>
<span>Imported by {service.initiated_by_detail.display_name}</span>
</div>
</div>
<CustomMenu ellipsis>

View file

@ -54,7 +54,10 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
const handleCommentDelete = async (commentId: string) => {
if (!workspaceSlug || !projectId || !issueId) return;
mutateIssueActivities((prevData) => prevData?.filter((p) => p.id !== commentId), false);
mutateIssueActivities(
(prevData: any) => prevData?.filter((p: any) => p.id !== commentId),
false
);
await issuesService
.deleteIssueComment(
@ -122,7 +125,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.first_name}
alt={activityItem.actor_detail.display_name}
height={24}
width={24}
className="rounded-full"
@ -131,7 +134,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.first_name.charAt(0)}
{activityItem.actor_detail.display_name.charAt(0)}
</div>
)}
</div>
@ -150,8 +153,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
) : (
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
<a className="text-gray font-medium">
{activityItem.actor_detail.first_name}{" "}
{activityItem.actor_detail.last_name}
{activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}

View file

@ -77,7 +77,7 @@ export const IssueAttachments = () => {
<Tooltip
tooltipContent={`${
people?.find((person) => person.member.id === file.updated_by)?.member
.first_name ?? ""
.display_name ?? ""
} uploaded on ${renderLongDateFormat(file.updated_at)}`}
>
<span>

View file

@ -1,7 +1,6 @@
import React from "react";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import { mutate } from "swr";
@ -12,28 +11,18 @@ import issuesServices from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Loader, SecondaryButton } from "components/ui";
import { SecondaryButton } from "components/ui";
// types
import type { ICurrentUserResponse, IIssueComment } from "types";
// fetch-keys
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader className="mb-5">
<Loader.Item height="12rem" width="100%" />
</Loader>
),
});
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
const WrappedRemirrorRichTextEditor = React.forwardRef<
IRemirrorRichTextEditor,
IRemirrorRichTextEditor
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
TiptapEditor.displayName = "TiptapEditor";
const defaultValues: Partial<IIssueComment> = {
comment_json: "",
@ -51,6 +40,7 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
handleSubmit,
control,
setValue,
watch,
formState: { isSubmitting },
reset,
} = useForm<IIssueComment>({ defaultValues });
@ -97,17 +87,26 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="issue-comments-section">
<div id="tiptap-container" className="issue-comments-section">
<Controller
name="comment_json"
name="comment_html"
control={control}
render={({ field: { value } }) => (
<WrappedRemirrorRichTextEditor
value={value}
onJSONChange={(jsonValue) => setValue("comment_json", jsonValue)}
onHTMLChange={(htmlValue) => setValue("comment_html", htmlValue)}
placeholder="Enter your comment..."
render={({ field: { value, onChange } }) => (
<TiptapEditor
ref={editorRef}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("comment_html")
: value
}
customClassName="p-3 min-h-[50px]"
debouncedUpdatesEnabled={false}
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
setValue("comment_json", comment_json);
}}
/>
)}
/>

View file

@ -1,7 +1,5 @@
import React, { useEffect, useState } from "react";
import dynamic from "next/dynamic";
// react-hook-form
import { useForm } from "react-hook-form";
// icons
@ -15,17 +13,13 @@ import { CommentReaction } from "components/issues";
import { timeAgo } from "helpers/date-time.helper";
// types
import type { IIssueComment } from "types";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { ssr: false });
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
const WrappedRemirrorRichTextEditor = React.forwardRef<
IRemirrorRichTextEditor,
IRemirrorRichTextEditor
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
TiptapEditor.displayName = "TiptapEditor";
type Props = {
comment: IIssueComment;
@ -45,6 +39,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
formState: { isSubmitting },
handleSubmit,
setFocus,
watch,
setValue,
} = useForm<IIssueComment>({
defaultValues: comment,
@ -56,8 +51,8 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
onSubmit(formData);
editorRef.current?.setEditorValue(formData.comment_json);
showEditorRef.current?.setEditorValue(formData.comment_json);
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
};
useEffect(() => {
@ -70,7 +65,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
<img
src={comment.actor_detail.avatar}
alt={comment.actor_detail.first_name}
alt={comment.actor_detail.display_name}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
@ -79,7 +74,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{comment.actor_detail.first_name.charAt(0)}
{comment.actor_detail.display_name.charAt(0)}
</div>
)}
@ -93,8 +88,9 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
<div className="min-w-0 flex-1">
<div>
<div className="text-xs">
{comment.actor_detail.first_name}
{comment.actor_detail.is_bot ? "Bot" : " " + comment.actor_detail.last_name}
{comment.actor_detail.is_bot
? comment.actor_detail.first_name + " Bot"
: comment.actor_detail.display_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">
Commented {timeAgo(comment.created_at)}
@ -105,15 +101,18 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
onSubmit={handleSubmit(onEnter)}
>
<WrappedRemirrorRichTextEditor
value={comment.comment_html}
onBlur={(jsonValue, htmlValue) => {
setValue("comment_json", jsonValue);
setValue("comment_html", htmlValue);
}}
placeholder="Enter Your comment..."
ref={editorRef}
/>
<div id="tiptap-container">
<TiptapEditor
ref={editorRef}
value={watch("comment_html")}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3"
onChange={(comment_json: Object, comment_html: string) => {
setValue("comment_json", comment_json);
setValue("comment_html", comment_html);
}}
/>
</div>
<div className="flex gap-1 self-end">
<button
type="submit"
@ -132,14 +131,12 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
</div>
</form>
<div className={`${isEditing ? "hidden" : ""}`}>
<WrappedRemirrorRichTextEditor
<TiptapEditor
ref={showEditorRef}
value={comment.comment_html}
editable={false}
noBorder
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
ref={showEditorRef}
/>
<CommentReaction projectId={comment.project} commentId={comment.id} />
</div>
</div>

View file

@ -1,23 +1,16 @@
import { FC, useCallback, useEffect, useState } from "react";
import dynamic from "next/dynamic";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// hooks
import useReloadConfirmations from "hooks/use-reload-confirmation";
// components
import { Loader, TextArea } from "components/ui";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader>
<Loader.Item height="12rem" width="100%" />
</Loader>
),
});
import { TextArea } from "components/ui";
// types
import { IIssue } from "types";
import Tiptap from "components/tiptap";
import { useDebouncedCallback } from "use-debounce";
export interface IssueDescriptionFormValues {
name: string;
@ -40,7 +33,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
handleFormSubmit,
isAllowed,
}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
const [characterLimit, setCharacterLimit] = useState(false);
const { setShowAlert } = useReloadConfirmations();
@ -63,7 +56,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<IIssue>) => {
if (!formData.name || formData.name.length === 0 || formData.name.length > 255) return;
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
await handleFormSubmit({
name: formData.name ?? "",
@ -74,6 +67,14 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
[handleFormSubmit]
);
useEffect(() => {
if (isSubmitting === "submitted") {
setTimeout(async () => {
setIsSubmitting("saved");
}, 2000);
}
}, [isSubmitting]);
// reset form values
useEffect(() => {
if (!issue) return;
@ -83,6 +84,12 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
});
}, [issue, reset]);
const debouncedTitleSave = useDebouncedCallback(async () => {
setTimeout(async () => {
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
}, 500);
}, 1000);
return (
<div className="relative">
<div className="relative">
@ -92,11 +99,10 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
placeholder="Enter issue name"
register={register}
onFocus={() => setCharacterLimit(true)}
onBlur={() => {
onChange={(e) => {
setCharacterLimit(false);
setIsSubmitting(true);
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting(false));
setIsSubmitting("submitting");
debouncedTitleSave();
}}
required={true}
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
@ -106,9 +112,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
{characterLimit && (
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
<span
className={`${
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
}`}
className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
}`}
>
{watch("name").length}
</span>
@ -117,47 +122,41 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
)}
</div>
<span>{errors.name ? errors.name.message : null}</span>
<div className="relative">
<div id="tiptap-container" className="relative">
<Controller
name="description"
name="description_html"
control={control}
render={({ field: { value } }) => {
render={({ field: { value, onChange } }) => {
if (!value && !watch("description_html")) return <></>;
return (
<RemirrorRichTextEditor
<Tiptap
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
onJSONChange={(jsonValue) => {
setShowAlert(true);
setValue("description", jsonValue);
debouncedUpdatesEnabled={true}
setIsSubmitting={setIsSubmitting}
customClassName="min-h-[150px]"
editorContentCustomClassNames="pb-9"
onChange={(description: Object, description_html: string) => {
setIsSubmitting("submitting");
onChange(description_html);
setValue("description", description);
handleSubmit(handleDescriptionFormSubmit)().finally(() => {
setIsSubmitting("submitted");
});
}}
onHTMLChange={(htmlValue) => {
setShowAlert(true);
setValue("description_html", htmlValue);
}}
onBlur={() => {
setIsSubmitting(true);
handleSubmit(handleDescriptionFormSubmit)()
.then(() => setShowAlert(false))
.finally(() => setIsSubmitting(false));
}}
placeholder="Description"
editable={isAllowed}
/>
);
}}
/>
{isSubmitting && (
<div className="absolute bottom-1 right-1 text-xs text-custom-text-200 bg-custom-background-100 p-3 z-10">
Saving...
</div>
)}
<div className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === 'saved' ? 'fadeOut' : 'fadeIn'}`}>
{isSubmitting === 'submitting' ? 'Saving...' : 'Saved'}
</div>
</div>
</div>
);

View file

@ -1,6 +1,5 @@
import React, { FC, useState, useEffect, useRef } from "react";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
// react-hook-form
@ -36,24 +35,14 @@ import {
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
// rich-text-editor
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader className="mt-4">
<Loader.Item height="12rem" width="100%" />
</Loader>
),
});
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
const WrappedRemirrorRichTextEditor = React.forwardRef<
IRemirrorRichTextEditor,
IRemirrorRichTextEditor
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
TiptapEditor.displayName = "TiptapEditor";
const defaultValues: Partial<IIssue> = {
project: "",
@ -75,6 +64,7 @@ const defaultValues: Partial<IIssue> = {
assignees_list: [],
labels: [],
labels_list: [],
start_date: null,
target_date: null,
};
@ -96,6 +86,7 @@ export interface IssueFormProps {
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
@ -239,6 +230,15 @@ export const IssueForm: FC<IssueFormProps> = ({
});
}, [getValues, projectId, reset]);
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
return (
<>
{projectId && (
@ -333,7 +333,7 @@ export const IssueForm: FC<IssueFormProps> = ({
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
<div className="relative">
<div id="tiptap-container" className="relative">
<div className="flex justify-end">
{issueName && issueName !== "" && (
<button
@ -363,21 +363,30 @@ export const IssueForm: FC<IssueFormProps> = ({
</button>
</div>
<Controller
name="description"
name="description_html"
control={control}
render={({ field: { value } }) => (
<WrappedRemirrorRichTextEditor
value={
!value || (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
placeholder="Description"
ref={editorRef}
/>
)}
render={({ field: { value, onChange } }) => {
if (!value && !watch("description_html")) return <></>;
return (
<TiptapEditor
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
customClassName="min-h-[150px]"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
setValue("description", description);
}}
/>
);
}}
/>
<GptAssistantModal
isOpen={gptAssistantModal}
@ -447,13 +456,34 @@ export const IssueForm: FC<IssueFormProps> = ({
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
<div>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Start date"
maxDate={maxDate ?? undefined}
onChange={onChange}
value={value}
/>
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
<div>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect value={value} onChange={onChange} />
<IssueDateSelect
label="Due date"
minDate={minDate ?? undefined}
onChange={onChange}
value={value}
/>
)}
/>
</div>

View file

@ -1,20 +1,27 @@
import { FC } from "react";
// next imports
import Link from "next/link";
import { useRouter } from "next/router";
// components
import { GanttChartRoot } from "components/gantt-chart";
// ui
import { Tooltip } from "components/ui";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user";
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
// components
import {
GanttChartRoot,
IssueGanttBlock,
renderIssueBlocksStructure,
} from "components/gantt-chart";
// types
import { IIssue } from "types";
type Props = {};
export const IssueGanttChartView: FC<Props> = ({}) => {
export const IssueGanttChartView = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { orderBy } = useIssuesView();
const { user } = useUser();
const { ganttIssues, mutateGanttIssues } = useGanttChartIssues(
workspaceSlug as string,
projectId as string
@ -31,76 +38,19 @@ export const IssueGanttChartView: FC<Props> = ({}) => {
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: any) => (
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${data?.id}`}>
<a className="relative flex items-center w-full h-full overflow-hidden shadow-sm font-normal">
<div
className="flex-shrink-0 w-[4px] h-full"
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
/>
<Tooltip tooltipContent={data?.name} className={`z-[999999]`}>
<div className="text-custom-text-100 text-[15px] whitespace-nowrap py-[4px] px-2.5 overflow-hidden w-full">
{data?.name}
</div>
</Tooltip>
{data.infoToggle && (
<Tooltip
tooltipContent={`No due-date set, rendered according to last updated date.`}
className={`z-[999999]`}
>
<div className="flex-shrink-0 mx-2 w-[18px] h-[18px] overflow-hidden flex justify-center items-center">
<span className="material-symbols-rounded text-custom-text-200 text-[18px]">
info
</span>
</div>
</Tooltip>
)}
</a>
</Link>
);
// handle gantt issue start date and target date
const handleUpdateDates = async (data: any) => {
const payload = {
id: data?.id,
start_date: data?.start_date,
target_date: data?.target_date,
};
};
const blockFormat = (blocks: any) =>
blocks && blocks.length > 0
? blocks.map((_block: any) => {
let startDate = new Date(_block.created_at);
let targetDate = new Date(_block.updated_at);
let infoToggle = true;
if (_block?.start_date && _block.target_date) {
startDate = _block?.start_date;
targetDate = _block.target_date;
infoToggle = false;
}
return {
start_date: new Date(startDate),
target_date: new Date(targetDate),
infoToggle: infoToggle,
data: _block,
};
})
: [];
return (
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
blockUpdateHandler={handleUpdateDates}
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) =>
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
blockRender={(data: any) => <IssueGanttBlock issue={data as IIssue} />}
enableReorder={orderBy === "sort_order"}
/>
</div>
);

View file

@ -18,7 +18,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ issue, maxRender = 1 }) => (
{issue.label_details.map((label, index) => (
<div
key={label.id}
className="flex cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
>
<Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name}>
<div className="flex items-center gap-1.5 text-custom-text-200">
@ -35,7 +35,7 @@ export const ViewIssueLabel: React.FC<Props> = ({ issue, maxRender = 1 }) => (
))}
</>
) : (
<div className="flex cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
<div className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
<Tooltip
position="top"
tooltipHeading="Labels"

View file

@ -50,11 +50,11 @@ export const IssueMainContent: React.FC<Props> = ({
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
workspaceSlug && projectId && issueDetails?.parent
? () =>
issuesService.subIssues(
workspaceSlug as string,
projectId as string,
issueDetails.parent ?? ""
)
issuesService.subIssues(
workspaceSlug as string,
projectId as string,
issueDetails.parent ?? ""
)
: null
);
const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id);
@ -97,9 +97,8 @@ export const IssueMainContent: React.FC<Props> = ({
<CustomMenu.MenuItem
key={issue.id}
renderAs="a"
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
issue.id
}`}
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id
}`}
className="flex items-center gap-2 py-2"
>
<LayerDiagonalIcon className="h-4 w-4" />

View file

@ -53,6 +53,7 @@ export interface IssuesModalProps {
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
@ -124,19 +125,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onClose]);
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !activeProject) return;
@ -260,7 +248,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
await addIssueToModule(res.id, payload.module);
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "gantt_chart") mutate(ganttFetchKey);
if (issueView === "gantt_chart")
mutate(ganttFetchKey, {
start_target_date: true,
order_by: "sort_order",
});
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();

View file

@ -120,7 +120,7 @@ export const MyIssuesViewOptions: React.FC = () => {
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"

View file

@ -200,7 +200,7 @@ export const MyIssuesView: React.FC<Props> = ({
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
);
const filtersToDisplay = { ...filters, assignees: null, created_by: null };
const filtersToDisplay = { ...filters, assignees: null, created_by: null, subscriber: null };
const nullFilters = Object.keys(filtersToDisplay).filter(
(key) => filtersToDisplay[key as keyof IIssueFilterOptions] === null
@ -264,7 +264,11 @@ export const MyIssuesView: React.FC<Props> = ({
disableUserActions={disableUserActions}
dragDisabled={groupBy !== "priority"}
emptyState={{
title: "You don't have any issue assigned to you yet",
title: filters.assignees
? "You don't have any issue assigned to you yet"
: filters.created_by
? "You have not created any issue yet."
: "You have not subscribed to any issue yet.",
description: "Keep track of your work in a single place.",
primaryButton: {
icon: <PlusIcon className="h-4 w-4" />,

View file

@ -30,20 +30,11 @@ export const IssueAssigneeSelect: React.FC<Props> = ({ projectId, value = [], on
const options = members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
query: member.member.display_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{`${
member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email
} ${member.member.last_name ?? ""}`}
{member.member.is_bot ? member.member.first_name : member.member.display_name}
</div>
),
}));

View file

@ -8,11 +8,14 @@ import DatePicker from "react-datepicker";
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
type Props = {
value: string | null;
label: string;
maxDate?: Date;
minDate?: Date;
onChange: (val: string | null) => void;
value: string | null;
};
export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onChange, value }) => (
<Popover className="relative flex items-center justify-center rounded-lg">
{({ open }) => (
<>
@ -28,7 +31,7 @@ export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
) : (
<>
<CalendarDaysIcon className="h-3.5 w-3.5 flex-shrink-0" />
<span>Due Date</span>
<span>{label}</span>
</>
)}
</span>
@ -51,6 +54,8 @@ export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
else onChange(renderDateFormat(val));
}}
dateFormat="dd-MM-yyyy"
minDate={minDate}
maxDate={maxDate}
inline
/>
</Popover.Panel>

View file

@ -41,20 +41,11 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({
const options = members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
query: member.member.display_name,
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{`${
member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email
} ${member.member.last_name ?? ""}`}
{member.member.display_name}
</div>
),
}));

View file

@ -85,7 +85,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
.then((res) => {
reset(defaultValues);
issueLabelMutate((prevData) => [...(prevData ?? []), res], false);
issueLabelMutate((prevData: any) => [...(prevData ?? []), res], false);
submitChanges({ labels_list: [...(issueDetails?.labels ?? []), res.id] });

View file

@ -54,6 +54,7 @@ type Props = {
| "parent"
| "blocker"
| "blocked"
| "startDate"
| "dueDate"
| "cycle"
| "module"
@ -210,6 +211,15 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
fieldsToShow.includes("cycle") ||
fieldsToShow.includes("module");
const startDate = watchIssue("start_date");
const targetDate = watchIssue("target_date");
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
return (
@ -367,6 +377,34 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
disabled={uneditable}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0" />
<p>Start date</p>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name="start_date"
render={({ field: { value } }) => (
<CustomDatePicker
placeholder="Start date"
value={value}
onChange={(val) =>
submitChanges({
start_date: val,
})
}
className="bg-custom-background-90 w-full"
maxDate={maxDate ?? undefined}
disabled={isNotAllowed || uneditable}
/>
)}
/>
</div>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
@ -386,7 +424,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
target_date: val,
})
}
className="bg-custom-background-90"
className="bg-custom-background-90 w-full"
minDate={minDate ?? undefined}
disabled={isNotAllowed || uneditable}
/>
)}

View file

@ -93,13 +93,14 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
});
};
const completedSubIssues = subIssuesResponse
? subIssuesResponse.state_distribution.completed +
subIssuesResponse.state_distribution.cancelled
: 0;
const completedSubIssue = subIssuesResponse?.state_distribution.completed ?? 0;
const cancelledSubIssue = subIssuesResponse?.state_distribution.cancelled ?? 0;
const totalCompletedSubIssues = completedSubIssue + cancelledSubIssue;
const totalSubIssues = subIssuesResponse ? subIssuesResponse.sub_issues.length : 0;
const completionPercentage = (completedSubIssues / totalSubIssues) * 100;
const completionPercentage = (totalCompletedSubIssues / totalSubIssues) * 100;
const isNotAllowed = memberRole.isGuest || memberRole.isViewer || disabled;

View file

@ -8,7 +8,7 @@ import useSWR from "swr";
import projectService from "services/project.service";
import trackEventServices from "services/track-event.service";
// ui
import { AssigneesList, Avatar, CustomSearchSelect, Tooltip } from "components/ui";
import { AssigneesList, Avatar, CustomSearchSelect, Icon, Tooltip } from "components/ui";
// icons
import { UserGroupIcon } from "@heroicons/react/24/outline";
// types
@ -47,20 +47,11 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
const options = members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
query: member.member.display_name,
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{`${
member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email
} ${member.member.last_name ?? ""}`}
{member.member.display_name}
</div>
),
}));
@ -71,11 +62,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
tooltipHeading="Assignees"
tooltipContent={
issue.assignee_details.length > 0
? issue.assignee_details
.map((assignee) =>
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
)
.join(", ")
? issue.assignee_details.map((assignee) => assignee?.display_name).join(", ")
: "No Assignee"
}
>
@ -86,11 +73,11 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
>
{issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? (
<div className="-my-0.5 flex items-center justify-center gap-2">
<AssigneesList userIds={issue.assignees} length={5} showLength={true} />
<AssigneesList userIds={issue.assignees} length={3} showLength={true} />
</div>
) : (
<div className="flex items-center justify-center gap-2">
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
<div className="flex items-center justify-center gap-2 px-1.5 py-1 rounded shadow-sm border border-custom-border-300">
<Icon iconName="person" className="text-sm !leading-4" />
</div>
)}
</div>
@ -100,6 +87,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
return (
<CustomSearchSelect
value={issue.assignees}
buttonClassName="!p-0"
onChange={(data: any) => {
const newData = issue.assignees ?? [];

View file

@ -32,9 +32,12 @@ export const ViewDueDateSelect: React.FC<Props> = ({
const { issueView } = useIssuesView();
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
return (
<Tooltip
tooltipHeading="Due Date"
tooltipHeading="Due date"
tooltipContent={
issue.target_date ? renderShortDateWithYearFormat(issue.target_date) ?? "N/A" : "N/A"
}
@ -56,8 +59,6 @@ export const ViewDueDateSelect: React.FC<Props> = ({
partialUpdateIssue(
{
target_date: val,
priority: issue.priority,
state: issue.state,
},
issue
);
@ -77,6 +78,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
className={`${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"} ${
issueView === "kanban" ? "bg-custom-background-90" : "bg-custom-background-100"
}`}
minDate={minDate ?? undefined}
noBorder={noBorder}
disabled={isNotAllowed}
/>

View file

@ -1,6 +1,7 @@
export * from "./assignee";
export * from "./due-date";
export * from "./estimate";
export * from "./priority";
export * from "./state";
export * from "./label";
export * from "./priority";
export * from "./start-date";
export * from "./state";

View file

@ -67,14 +67,8 @@ export const ViewPrioritySelect: React.FC<Props> = ({
noBorder
? ""
: issue.priority === "urgent"
? "border-red-500/20 bg-red-500/20"
: issue.priority === "high"
? "border-orange-500/20 bg-orange-500/20"
: issue.priority === "medium"
? "border-yellow-500/20 bg-yellow-500/20"
: issue.priority === "low"
? "border-green-500/20 bg-green-500/20"
: "border-custom-border-200 bg-custom-background-80"
? "border-red-500/20 bg-red-500"
: "border-custom-border-300 bg-custom-background-100"
} items-center`}
>
<Tooltip
@ -87,7 +81,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
`text-sm ${
issue.priority === "urgent"
? "text-red-500"
? "text-white"
: issue.priority === "high"
? "text-orange-500"
: issue.priority === "medium"

View file

@ -0,0 +1,80 @@
import { useRouter } from "next/router";
// ui
import { CustomDatePicker, Tooltip } from "components/ui";
// helpers
import { findHowManyDaysLeft, renderShortDateWithYearFormat } from "helpers/date-time.helper";
// services
import trackEventServices from "services/track-event.service";
// types
import { ICurrentUserResponse, IIssue } from "types";
import useIssuesView from "hooks/use-issues-view";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
tooltipPosition?: "top" | "bottom";
noBorder?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
export const ViewStartDateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
tooltipPosition = "top",
noBorder = false,
user,
isNotAllowed,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { issueView } = useIssuesView();
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
return (
<Tooltip
tooltipHeading="Start date"
tooltipContent={
issue.start_date ? renderShortDateWithYearFormat(issue.start_date) ?? "N/A" : "N/A"
}
position={tooltipPosition}
>
<div className="group flex-shrink-0 relative max-w-[6.5rem]">
<CustomDatePicker
placeholder="Start date"
value={issue?.start_date}
onChange={(val) => {
partialUpdateIssue(
{
start_date: val,
},
issue
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_DUE_DATE",
user
);
}}
className={`${issue?.start_date ? "w-[6.5rem]" : "w-[5rem] text-center"} ${
issueView === "kanban" ? "bg-custom-background-90" : "bg-custom-background-100"
}`}
maxDate={maxDate ?? undefined}
noBorder={noBorder}
disabled={isNotAllowed}
/>
</div>
</Tooltip>
);
};

View file

@ -74,9 +74,9 @@ export const ViewStateSelect: React.FC<Props> = ({
position={tooltipPosition}
>
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
<span className="h-4 w-4">
<span className="h-3.5 w-3.5">
{selectedOption &&
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
getStateGroupIcon(selectedOption.group, "14", "14", selectedOption.color)}
</span>
<span className="truncate">{selectedOption?.name ?? "State"}</span>
</div>
@ -131,6 +131,7 @@ export const ViewStateSelect: React.FC<Props> = ({
disabled={isNotAllowed}
onOpen={() => setFetchStates(true)}
noChevron
selfPositioned={selfPositioned}
/>
);
};

View file

@ -20,6 +20,7 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline";
import type { ICurrentUserResponse, IIssueLabels, IState } from "types";
// constants
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
// types
type Props = {
@ -52,10 +53,15 @@ export const CreateLabelModal: React.FC<Props> = ({
watch,
control,
reset,
setValue,
} = useForm<IIssueLabels>({
defaultValues,
});
useEffect(() => {
if (isOpen) setValue("color", getRandomLabelColor());
}, [setValue, isOpen]);
const onClose = () => {
handleClose();
reset(defaultValues);
@ -156,6 +162,7 @@ export const CreateLabelModal: React.FC<Props> = ({
render={({ field: { value, onChange } }) => (
<TwitterPicker
color={value}
colors={LABEL_COLOR_OPTIONS}
onChange={(value) => {
onChange(value.hex);
close();

View file

@ -22,12 +22,14 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
type Props = {
labelForm: boolean;
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
isUpdating: boolean;
labelToUpdate: IIssueLabels | null;
onClose?: () => void;
};
const defaultValues: Partial<IIssueLabels> = {
@ -35,167 +37,180 @@ const defaultValues: Partial<IIssueLabels> = {
color: "rgb(var(--color-text-200))",
};
type Ref = HTMLDivElement;
export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
function CreateUpdateLabelInline(props, ref) {
const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props;
export const CreateUpdateLabelInline = forwardRef<Ref, Props>(function CreateUpdateLabelInline(
{ labelForm, setLabelForm, isUpdating, labelToUpdate },
ref
) {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth();
const { user } = useUserAuth();
const {
handleSubmit,
control,
register,
reset,
formState: { errors, isSubmitting },
watch,
setValue,
} = useForm<IIssueLabels>({
defaultValues,
});
const {
handleSubmit,
control,
register,
reset,
formState: { errors, isSubmitting },
watch,
setValue,
} = useForm<IIssueLabels>({
defaultValues,
});
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return;
const handleClose = () => {
setLabelForm(false);
reset(defaultValues);
if (onClose) onClose();
};
await issuesService
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
.then((res) => {
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId as string),
(prevData) => [res, ...(prevData ?? [])],
false
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return;
await issuesService
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
.then((res) => {
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId as string),
(prevData) => [res, ...(prevData ?? [])],
false
);
handleClose();
});
};
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return;
await issuesService
.patchIssueLabel(
workspaceSlug as string,
projectId as string,
labelToUpdate?.id ?? "",
formData,
user
)
.then(() => {
reset(defaultValues);
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId as string),
(prevData) =>
prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)),
false
);
handleClose();
});
};
useEffect(() => {
if (!labelForm && isUpdating) return;
reset();
}, [labelForm, isUpdating, reset]);
useEffect(() => {
if (!labelToUpdate) return;
setValue(
"color",
labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"
);
setValue("name", labelToUpdate.name);
}, [labelToUpdate, setValue]);
useEffect(() => {
if (labelToUpdate) {
setValue(
"color",
labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"
);
reset(defaultValues);
setLabelForm(false);
});
};
return;
}
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return;
setValue("color", getRandomLabelColor());
}, [labelToUpdate, setValue]);
await issuesService
.patchIssueLabel(
workspaceSlug as string,
projectId as string,
labelToUpdate?.id ?? "",
formData,
user
)
.then(() => {
reset(defaultValues);
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId as string),
(prevData) =>
prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)),
false
);
setLabelForm(false);
});
};
useEffect(() => {
if (!labelForm && isUpdating) return;
reset();
}, [labelForm, isUpdating, reset]);
useEffect(() => {
if (!labelToUpdate) return;
setValue(
"color",
labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"
);
setValue("name", labelToUpdate.name);
}, [labelToUpdate, setValue]);
return (
<div
className={`flex scroll-m-8 items-center gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 ${
labelForm ? "" : "hidden"
}`}
ref={ref}
>
<div className="flex-shrink-0">
<Popover className="relative z-10 flex h-full w-full items-center justify-center">
{({ open }) => (
<>
<Popover.Button
className={`group inline-flex items-center text-base font-medium focus:outline-none ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
<span
className="h-5 w-5 rounded"
style={{
backgroundColor: watch("color"),
}}
/>
<ChevronDownIcon
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
open ? "text-gray-600" : "text-gray-400"
}`}
aria-hidden="true"
/>
</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 top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
<Controller
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
)}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
<div className="flex flex-1 flex-col justify-center">
<Input
type="text"
id="labelName"
name="name"
register={register}
placeholder="Label title"
validations={{
required: "Label title is required",
}}
error={errors.name}
/>
</div>
<SecondaryButton
onClick={() => {
reset();
setLabelForm(false);
}}
return (
<div
className={`flex scroll-m-8 items-center gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 ${
labelForm ? "" : "hidden"
}`}
ref={ref}
>
Cancel
</SecondaryButton>
{isUpdating ? (
<PrimaryButton onClick={handleSubmit(handleLabelUpdate)} loading={isSubmitting}>
{isSubmitting ? "Updating" : "Update"}
</PrimaryButton>
) : (
<PrimaryButton onClick={handleSubmit(handleLabelCreate)} loading={isSubmitting}>
{isSubmitting ? "Adding" : "Add"}
</PrimaryButton>
)}
</div>
);
});
<div className="flex-shrink-0">
<Popover className="relative z-10 flex h-full w-full items-center justify-center">
{({ open }) => (
<>
<Popover.Button
className={`group inline-flex items-center text-base font-medium focus:outline-none ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
<span
className="h-5 w-5 rounded"
style={{
backgroundColor: watch("color"),
}}
/>
<ChevronDownIcon
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
open ? "text-gray-600" : "text-gray-400"
}`}
aria-hidden="true"
/>
</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 top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
<Controller
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<TwitterPicker
colors={LABEL_COLOR_OPTIONS}
color={value}
onChange={(value) => onChange(value.hex)}
/>
)}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
<div className="flex flex-1 flex-col justify-center">
<Input
type="text"
id="labelName"
name="name"
register={register}
placeholder="Label title"
validations={{
required: "Label title is required",
}}
error={errors.name}
/>
</div>
<SecondaryButton onClick={() => handleClose()}>Cancel</SecondaryButton>
{isUpdating ? (
<PrimaryButton onClick={handleSubmit(handleLabelUpdate)} loading={isSubmitting}>
{isSubmitting ? "Updating" : "Update"}
</PrimaryButton>
) : (
<PrimaryButton onClick={handleSubmit(handleLabelCreate)} loading={isSubmitting}>
{isSubmitting ? "Adding" : "Add"}
</PrimaryButton>
)}
</div>
);
}
);

View file

@ -49,8 +49,8 @@ export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent,
if (!workspaceSlug || !projectId) return;
mutate(
(prevData) =>
prevData?.map((l) => {
(prevData: any) =>
prevData?.map((l: any) => {
if (l.id === label.id) return { ...l, parent: parent?.id ?? "" };
return l;

Some files were not shown because too many files have changed in this diff Show more