forked from github/plane
feat: spreadsheet view (#1369)
* feat: spreadsheet view * fix: fix scroll and overflow issues, feat: updated issue properties component, style: ui improvements * feat: sub-issue toggle and sub-issue hook added, chore: code refactor * fix: only render parent issue * feat: sub issue fetching hook updated and nested sub issue added, chore: code refactor * style: title sticky to left on scroll and column styling * fix: tooltip , filter and view z-index fix * feat: spreadsheet view column sorting, fix: sticky scroll issue fix * feat: updated issue view filter for spreadsheet view * style: spreadsheet view column * feat: double click to edit title * fix: estimate sorting fix * style: spreadsheet view columns * fix: spreadsheet view mutation, feat: edit , copy and delete option added * fix: edit sub issue fix
This commit is contained in:
parent
0cb856b92f
commit
e08fc59114
@ -2,6 +2,7 @@ export * from "./board-view";
|
|||||||
export * from "./calendar-view";
|
export * from "./calendar-view";
|
||||||
export * from "./gantt-chart-view";
|
export * from "./gantt-chart-view";
|
||||||
export * from "./list-view";
|
export * from "./list-view";
|
||||||
|
export * from "./spreadsheet-view";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./bulk-delete-issues-modal";
|
export * from "./bulk-delete-issues-modal";
|
||||||
export * from "./existing-issues-list-modal";
|
export * from "./existing-issues-list-modal";
|
||||||
|
@ -10,7 +10,7 @@ import { Popover, Transition } from "@headlessui/react";
|
|||||||
// components
|
// components
|
||||||
import { SelectFilters } from "components/views";
|
import { SelectFilters } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, ToggleSwitch } from "components/ui";
|
import { CustomMenu, Icon, ToggleSwitch } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
@ -83,6 +83,15 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
|
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||||
|
issueView === "spreadsheet" ? "bg-brand-surface-2" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setIssueView("spreadsheet")}
|
||||||
|
>
|
||||||
|
<Icon iconName="table_chart" className="text-brand-secondary" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded outline-none duration-300 hover:bg-brand-surface-2 ${
|
className={`grid h-7 w-7 place-items-center rounded outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||||
@ -146,10 +155,10 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2 divide-brand-base">
|
<div className="relative divide-y-2 divide-brand-base">
|
||||||
<div className="space-y-4 pb-3 text-xs">
|
<div className="space-y-4 pb-3 text-xs">
|
||||||
{issueView !== "calendar" && (
|
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-brand-secondary">Group by</h4>
|
<h4 className="text-brand-secondary">Group by</h4>
|
||||||
@ -221,7 +230,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{issueView !== "calendar" && (
|
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-brand-secondary">Show empty states</h4>
|
<h4 className="text-brand-secondary">Show empty states</h4>
|
||||||
@ -252,6 +261,13 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
{Object.keys(properties).map((key) => {
|
{Object.keys(properties).map((key) => {
|
||||||
if (key === "estimate" && !isEstimateActive) return null;
|
if (key === "estimate" && !isEstimateActive) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(issueView === "spreadsheet" && key === "sub_issue_count") ||
|
||||||
|
key === "attachment_count" ||
|
||||||
|
key === "link"
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
|
@ -19,7 +19,14 @@ import useToast from "hooks/use-toast";
|
|||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// components
|
// components
|
||||||
import { AllLists, AllBoards, FilterList, CalendarView, GanttChartView } from "components/core";
|
import {
|
||||||
|
AllLists,
|
||||||
|
AllBoards,
|
||||||
|
FilterList,
|
||||||
|
CalendarView,
|
||||||
|
GanttChartView,
|
||||||
|
SpreadsheetView,
|
||||||
|
} from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
import { CycleIssuesGanttChartView, TransferIssues, TransferIssuesModal } from "components/cycles";
|
import { CycleIssuesGanttChartView, TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||||
@ -563,6 +570,13 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
user={user}
|
user={user}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
/>
|
/>
|
||||||
|
) : issueView === "spreadsheet" ? (
|
||||||
|
<SpreadsheetView
|
||||||
|
handleEditIssue={handleEditIssue}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
user={user}
|
||||||
|
userAuth={memberRole}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
issueView === "gantt_chart" && <GanttChartView />
|
issueView === "gantt_chart" && <GanttChartView />
|
||||||
)}
|
)}
|
||||||
|
4
apps/app/components/core/spreadsheet-view/index.ts
Normal file
4
apps/app/components/core/spreadsheet-view/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./spreadsheet-view";
|
||||||
|
export * from "./single-issue";
|
||||||
|
export * from "./spreadsheet-columns";
|
||||||
|
export * from "./spreadsheet-issues";
|
266
apps/app/components/core/spreadsheet-view/single-issue.tsx
Normal file
266
apps/app/components/core/spreadsheet-view/single-issue.tsx
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
ViewAssigneeSelect,
|
||||||
|
ViewDueDateSelect,
|
||||||
|
ViewEstimateSelect,
|
||||||
|
ViewPrioritySelect,
|
||||||
|
ViewStateSelect,
|
||||||
|
} from "components/issues";
|
||||||
|
// icons
|
||||||
|
import { CustomMenu, Icon } from "components/ui";
|
||||||
|
import { LinkIcon, PencilIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
// hooks
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// constant
|
||||||
|
import {
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
VIEW_ISSUES,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||||
|
// helper
|
||||||
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
expanded: boolean;
|
||||||
|
handleToggleExpand: (issueId: string) => void;
|
||||||
|
properties: Properties;
|
||||||
|
handleEditIssue: () => void;
|
||||||
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
gridTemplateColumns: string;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
userAuth: UserAuth;
|
||||||
|
nestingLevel: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
expanded,
|
||||||
|
handleToggleExpand,
|
||||||
|
properties,
|
||||||
|
handleEditIssue,
|
||||||
|
handleDeleteIssue,
|
||||||
|
gridTemplateColumns,
|
||||||
|
user,
|
||||||
|
userAuth,
|
||||||
|
nestingLevel,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
|
const { params } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const partialUpdateIssue = useCallback(
|
||||||
|
(formData: Partial<IIssue>, issueId: string) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
const fetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), params)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
||||||
|
|
||||||
|
mutate<IIssue[]>(
|
||||||
|
fetchKey,
|
||||||
|
(prevData) =>
|
||||||
|
(prevData ?? []).map((p) => {
|
||||||
|
if (p.id === issueId) {
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
issuesService
|
||||||
|
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user)
|
||||||
|
.then(() => {
|
||||||
|
mutate(fetchKey);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCopyText = () => {
|
||||||
|
const originURL =
|
||||||
|
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
|
copyTextToClipboard(
|
||||||
|
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||||
|
).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link Copied!",
|
||||||
|
message: "Issue link copied to clipboard.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const paddingLeft = `${nestingLevel * 68}px`;
|
||||||
|
|
||||||
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-brand-surface-2 border-b border-brand-base w-full min-w-max"
|
||||||
|
style={{ gridTemplateColumns }}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1.5 items-center px-4 sticky left-0 z-10 text-brand-secondary bg-brand-base group-hover:text-brand-base group-hover:bg-brand-surface-2 border-brand-base w-full">
|
||||||
|
<span className="flex gap-1 items-center" style={issue.parent ? { paddingLeft } : {}}>
|
||||||
|
{properties.key && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center cursor-pointer text-xs text-center hover:text-brand-base w-14 ">
|
||||||
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="h-5 w-5">
|
||||||
|
{issue.sub_issues_count > 0 && (
|
||||||
|
<button
|
||||||
|
className="h-5 w-5 hover:bg-brand-surface-1 hover:text-brand-base rounded-sm"
|
||||||
|
onClick={() => handleToggleExpand(issue.id)}
|
||||||
|
>
|
||||||
|
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||||
|
<a className="truncate text-brand-base cursor-pointer w-full text-[0.825rem]">
|
||||||
|
{issue.name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{properties.state && (
|
||||||
|
<div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||||
|
<ViewStateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
customButton
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.priority && (
|
||||||
|
<div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||||
|
<ViewPrioritySelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.assignee && (
|
||||||
|
<div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||||
|
<ViewAssigneeSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
customButton
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.labels ? (
|
||||||
|
issue.label_details.length > 0 ? (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||||
|
{issue.label_details.slice(0, 4).map((label, index) => (
|
||||||
|
<div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}>
|
||||||
|
<span
|
||||||
|
className={`h-4 w-4 flex-shrink-0 rounded-full border group-hover:bg-brand-surface-2 border-brand-base
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: label?.color && label.color !== "" ? label.color : "#000000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{issue.label_details.length > 4 ? <span>+{issue.label_details.length - 4}</span> : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||||
|
No Labels
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
{properties.due_date && (
|
||||||
|
<div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||||
|
<ViewDueDateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.estimate && (
|
||||||
|
<div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
|
||||||
|
<ViewEstimateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute top-2.5 right-2.5 z-30 cursor-pointer opacity-0 group-hover:opacity-100">
|
||||||
|
{!isNotAllowed && (
|
||||||
|
<CustomMenu width="auto" ellipsis>
|
||||||
|
<CustomMenu.MenuItem onClick={handleEditIssue}>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<PencilIcon className="h-4 w-4" />
|
||||||
|
<span>Edit issue</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
<span>Delete issue</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<LinkIcon className="h-4 w-4" />
|
||||||
|
<span>Copy issue link</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,241 @@
|
|||||||
|
import React from "react";
|
||||||
|
// hooks
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
|
// component
|
||||||
|
import { CustomMenu, Icon } from "components/ui";
|
||||||
|
// icon
|
||||||
|
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { TIssueOrderByOptions } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
columnData: any;
|
||||||
|
gridTemplateColumns: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateColumns }) => {
|
||||||
|
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
||||||
|
"spreadsheetViewSorting",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
const { orderBy, setOrderBy } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
|
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
||||||
|
setOrderBy(order);
|
||||||
|
setSelectedMenuItem(`${order}_${itemKey}`);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`grid auto-rows-[minmax(36px,1fr)] w-full min-w-max`}
|
||||||
|
style={{ gridTemplateColumns }}
|
||||||
|
>
|
||||||
|
{columnData.map((col: any) => {
|
||||||
|
if (col.isActive) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`bg-brand-surface-2 ${
|
||||||
|
col.propertyName === "title" ? "sticky left-0 z-20 bg-brand-surface-2 pl-24" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{col.propertyName === "title" || col.propertyName === "priority" ? (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-start gap-1.5 cursor-default text-sm text-brand-secondary text-current py-2.5 px-2`}
|
||||||
|
>
|
||||||
|
{col.icon ? (
|
||||||
|
<col.icon
|
||||||
|
className={`text-brand-secondary ${
|
||||||
|
col.propertyName === "estimate" ? "-rotate-90" : ""
|
||||||
|
}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
height="14"
|
||||||
|
width="14"
|
||||||
|
/>
|
||||||
|
) : col.propertyName === "priority" ? (
|
||||||
|
<span className="text-sm material-symbols-rounded text-brand-secondary">
|
||||||
|
signal_cellular_alt
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
|
||||||
|
{col.colName}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CustomMenu
|
||||||
|
customButton={
|
||||||
|
<div
|
||||||
|
className={`group flex items-center justify-start gap-1.5 cursor-pointer text-sm text-brand-secondary text-current hover:text-brand-base py-2.5 px-2`}
|
||||||
|
>
|
||||||
|
{col.icon ? (
|
||||||
|
<col.icon
|
||||||
|
className={`text-brand-secondary group-hover:text-brand-base ${
|
||||||
|
col.propertyName === "estimate" ? "-rotate-90" : ""
|
||||||
|
}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
height="14"
|
||||||
|
width="14"
|
||||||
|
/>
|
||||||
|
) : col.propertyName === "priority" ? (
|
||||||
|
<span className="text-sm material-symbols-rounded text-brand-secondary">
|
||||||
|
signal_cellular_alt
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
|
||||||
|
{col.colName}
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
menuItemsWhiteBg
|
||||||
|
width="xl"
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className={`${
|
||||||
|
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
||||||
|
? "bg-brand-surface-2"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
key={col.propertyName}
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy(col.ascendingOrder, col.propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
||||||
|
? "text-brand-base"
|
||||||
|
: "text-brand-secondary hover:text-brand-base"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
|
||||||
|
<>
|
||||||
|
<span>A-Z</span>
|
||||||
|
<span>Ascending</span>
|
||||||
|
</>
|
||||||
|
) : col.propertyName === "due_date" ? (
|
||||||
|
<>
|
||||||
|
<span>1-9</span>
|
||||||
|
<span>Ascending</span>
|
||||||
|
</>
|
||||||
|
) : col.propertyName === "estimate" ? (
|
||||||
|
<>
|
||||||
|
<span>0</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>10</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>First</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>Last</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||||
|
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
||||||
|
? "opacity-100"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className={`${
|
||||||
|
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
||||||
|
? "bg-brand-surface-2"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
key={col.property}
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy(col.descendingOrder, col.propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
||||||
|
? "text-brand-base"
|
||||||
|
: "text-brand-secondary hover:text-brand-base"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
|
||||||
|
<>
|
||||||
|
<span>Z-A</span>
|
||||||
|
<span>Descending</span>
|
||||||
|
</>
|
||||||
|
) : col.propertyName === "due_date" ? (
|
||||||
|
<>
|
||||||
|
<span>9-1</span>
|
||||||
|
<span>Descending</span>
|
||||||
|
</>
|
||||||
|
) : col.propertyName === "estimate" ? (
|
||||||
|
<>
|
||||||
|
<span>10</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>0</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Last</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>First</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||||
|
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
||||||
|
? "opacity-100"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className={`${
|
||||||
|
selectedMenuItem === `-created_at_${col.propertyName}`
|
||||||
|
? "bg-brand-surface-2"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
key={col.property}
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy("-created_at", col.propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `-created_at_${col.propertyName}`
|
||||||
|
? "text-brand-base"
|
||||||
|
: "text-brand-secondary hover:text-brand-base"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
<Icon iconName="block" className="text-sm" />
|
||||||
|
<span>None</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||||
|
selectedMenuItem === `-created_at_${col.propertyName}`
|
||||||
|
? "opacity-100"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { SingleSpreadsheetIssue } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
key: string;
|
||||||
|
issue: IIssue;
|
||||||
|
expandedIssues: string[];
|
||||||
|
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
properties: Properties;
|
||||||
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
gridTemplateColumns: string;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
userAuth: UserAuth;
|
||||||
|
nestingLevel?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetIssues: React.FC<Props> = ({
|
||||||
|
key,
|
||||||
|
issue,
|
||||||
|
expandedIssues,
|
||||||
|
setExpandedIssues,
|
||||||
|
gridTemplateColumns,
|
||||||
|
properties,
|
||||||
|
handleEditIssue,
|
||||||
|
handleDeleteIssue,
|
||||||
|
user,
|
||||||
|
userAuth,
|
||||||
|
nestingLevel = 0,
|
||||||
|
}) => {
|
||||||
|
const handleToggleExpand = (issueId: string) => {
|
||||||
|
setExpandedIssues((prevState) => {
|
||||||
|
const newArray = [...prevState];
|
||||||
|
const index = newArray.indexOf(issueId);
|
||||||
|
if (index > -1) {
|
||||||
|
newArray.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
newArray.push(issueId);
|
||||||
|
}
|
||||||
|
return newArray;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SingleSpreadsheetIssue
|
||||||
|
issue={issue}
|
||||||
|
expanded={isExpanded}
|
||||||
|
handleToggleExpand={handleToggleExpand}
|
||||||
|
gridTemplateColumns={gridTemplateColumns}
|
||||||
|
properties={properties}
|
||||||
|
handleEditIssue={() => handleEditIssue(issue)}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
user={user}
|
||||||
|
userAuth={userAuth}
|
||||||
|
nestingLevel={nestingLevel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue, subIndex: number) => (
|
||||||
|
<SpreadsheetIssues
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
setExpandedIssues={setExpandedIssues}
|
||||||
|
gridTemplateColumns={gridTemplateColumns}
|
||||||
|
properties={properties}
|
||||||
|
handleEditIssue={() => handleEditIssue(subIssue)}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
user={user}
|
||||||
|
userAuth={userAuth}
|
||||||
|
nestingLevel={nestingLevel + 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,94 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
||||||
|
import { Icon, Spinner } from "components/ui";
|
||||||
|
// hooks
|
||||||
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||||
|
// constants
|
||||||
|
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
||||||
|
// icon
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
userAuth: UserAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetView: React.FC<Props> = ({
|
||||||
|
handleEditIssue,
|
||||||
|
handleDeleteIssue,
|
||||||
|
user,
|
||||||
|
userAuth,
|
||||||
|
}) => {
|
||||||
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { spreadsheetIssues } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
|
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
||||||
|
...column,
|
||||||
|
isActive: properties
|
||||||
|
? column.propertyName === "labels"
|
||||||
|
? properties[column.propertyName as keyof Properties]
|
||||||
|
: column.propertyName === "title"
|
||||||
|
? true
|
||||||
|
: properties[column.propertyName as keyof Properties]
|
||||||
|
: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const gridTemplateColumns = columnData
|
||||||
|
.filter((column) => column.isActive)
|
||||||
|
.map((column) => column.colSize)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full rounded-lg text-brand-secondary overflow-x-auto whitespace-nowrap bg-brand-base">
|
||||||
|
<div className="sticky z-20 top-0 border-b border-brand-base bg-brand-surface-2 w-full min-w-max">
|
||||||
|
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
||||||
|
</div>
|
||||||
|
{spreadsheetIssues ? (
|
||||||
|
<div className="flex flex-col h-full w-full bg-brand-base rounded-sm ">
|
||||||
|
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||||
|
<SpreadsheetIssues
|
||||||
|
key={`${issue.id}_${index}`}
|
||||||
|
issue={issue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
setExpandedIssues={setExpandedIssues}
|
||||||
|
gridTemplateColumns={gridTemplateColumns}
|
||||||
|
properties={properties}
|
||||||
|
handleEditIssue={handleEditIssue}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
user={user}
|
||||||
|
userAuth={userAuth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1.5 pl-7 py-2.5 text-sm text-brand-secondary hover:text-brand-base hover:bg-brand-surface-2 border-b border-brand-base w-full min-w-max"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
Add Issue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -12,6 +12,7 @@ import issueServices from "services/issues.service";
|
|||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
// icons
|
// icons
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
// ui
|
// ui
|
||||||
@ -41,6 +42,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
|||||||
|
|
||||||
const { issueView, params } = useIssuesView();
|
const { issueView, params } = useIssuesView();
|
||||||
const { params: calendarParams } = useCalendarIssuesView();
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
|
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -74,6 +76,20 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
|||||||
(prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
|
(prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
} else if (issueView === "spreadsheet") {
|
||||||
|
const spreadsheetFetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams);
|
||||||
|
|
||||||
|
mutate<IIssue[]>(
|
||||||
|
spreadsheetFetchKey,
|
||||||
|
(prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
|
||||||
|
false
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
||||||
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
||||||
|
@ -17,6 +17,7 @@ import useIssuesView from "hooks/use-issues-view";
|
|||||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useInboxView from "hooks/use-inbox-view";
|
import useInboxView from "hooks/use-inbox-view";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
// components
|
// components
|
||||||
import { IssueForm } from "components/issues";
|
import { IssueForm } from "components/issues";
|
||||||
// types
|
// types
|
||||||
@ -79,6 +80,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
const { params: calendarParams } = useCalendarIssuesView();
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
const { order_by, group_by, ...viewGanttParams } = params;
|
const { order_by, group_by, ...viewGanttParams } = params;
|
||||||
const { params: inboxParams } = useInboxView();
|
const { params: inboxParams } = useInboxView();
|
||||||
|
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
||||||
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
|
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
|
||||||
@ -211,6 +213,14 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams);
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams);
|
||||||
|
|
||||||
|
const spreadsheetFetchKey = cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
|
||||||
|
: moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
|
||||||
|
: viewId
|
||||||
|
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams);
|
||||||
|
|
||||||
const ganttFetchKey = cycleId
|
const ganttFetchKey = cycleId
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
|
||||||
: moduleId
|
: moduleId
|
||||||
@ -234,6 +244,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
|
|
||||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||||
if (issueView === "gantt_chart") mutate(ganttFetchKey);
|
if (issueView === "gantt_chart") mutate(ganttFetchKey);
|
||||||
|
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -264,6 +275,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
||||||
} else {
|
} else {
|
||||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||||
|
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ type Props = {
|
|||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
tooltipPosition?: "left" | "right";
|
tooltipPosition?: "left" | "right";
|
||||||
|
customButton?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
@ -34,6 +35,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
tooltipPosition = "right",
|
tooltipPosition = "right",
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
|
customButton = false,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -65,6 +67,38 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const assigneeLabel = (
|
||||||
|
<Tooltip
|
||||||
|
position={`top-${tooltipPosition}`}
|
||||||
|
tooltipHeading="Assignees"
|
||||||
|
tooltipContent={
|
||||||
|
issue.assignee_details.length > 0
|
||||||
|
? issue.assignee_details
|
||||||
|
.map((assignee) =>
|
||||||
|
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
|
||||||
|
)
|
||||||
|
.join(", ")
|
||||||
|
: "No Assignee"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex ${
|
||||||
|
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
|
} items-center gap-2 text-brand-secondary`}
|
||||||
|
>
|
||||||
|
{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} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<UserGroupIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={issue.assignees}
|
value={issue.assignees}
|
||||||
@ -90,37 +124,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
label={
|
{...(customButton ? { customButton: assigneeLabel } : { label: assigneeLabel })}
|
||||||
<Tooltip
|
|
||||||
position={`top-${tooltipPosition}`}
|
|
||||||
tooltipHeading="Assignees"
|
|
||||||
tooltipContent={
|
|
||||||
issue.assignee_details.length > 0
|
|
||||||
? issue.assignee_details
|
|
||||||
.map((assignee) =>
|
|
||||||
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
|
|
||||||
)
|
|
||||||
.join(", ")
|
|
||||||
: "No Assignee"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex ${
|
|
||||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
|
||||||
} items-center gap-2 text-brand-secondary`}
|
|
||||||
>
|
|
||||||
{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} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<UserGroupIcon className="h-4 w-4 text-brand-secondary" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
multiple
|
multiple
|
||||||
noChevron
|
noChevron
|
||||||
position={position}
|
position={position}
|
||||||
|
@ -12,6 +12,7 @@ import { ICurrentUserResponse, IIssue } from "types";
|
|||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
|
noBorder?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
@ -19,6 +20,7 @@ type Props = {
|
|||||||
export const ViewDueDateSelect: React.FC<Props> = ({
|
export const ViewDueDateSelect: React.FC<Props> = ({
|
||||||
issue,
|
issue,
|
||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
|
noBorder = false,
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
}) => {
|
}) => {
|
||||||
@ -62,6 +64,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className={issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}
|
className={issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}
|
||||||
|
noBorder={noBorder}
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,7 @@ type Props = {
|
|||||||
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
|
customButton?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
@ -27,6 +28,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
position = "left",
|
position = "left",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
|
customButton = false,
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
}) => {
|
}) => {
|
||||||
@ -37,6 +39,15 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
|
|
||||||
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
|
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
|
||||||
|
|
||||||
|
const estimateLabels = (
|
||||||
|
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}>
|
||||||
|
<div className="flex items-center gap-1 text-brand-secondary">
|
||||||
|
<PlayIcon className="h-3.5 w-3.5 -rotate-90" />
|
||||||
|
{estimateValue ?? "None"}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
if (!isEstimateActive) return null;
|
if (!isEstimateActive) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -57,14 +68,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
user
|
user
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
label={
|
{...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })}
|
||||||
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}>
|
|
||||||
<div className="flex items-center gap-1 text-brand-secondary">
|
|
||||||
<PlayIcon className="h-3.5 w-3.5 -rotate-90" />
|
|
||||||
{estimateValue ?? "Estimate"}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
maxHeight="md"
|
maxHeight="md"
|
||||||
noChevron
|
noChevron
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
|
@ -12,12 +12,15 @@ import { ICurrentUserResponse, IIssue } from "types";
|
|||||||
import { PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
// services
|
// services
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
// helper
|
||||||
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
|
noBorder?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
@ -27,6 +30,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
position = "left",
|
position = "left",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
|
noBorder = false,
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
}) => {
|
}) => {
|
||||||
@ -55,10 +59,12 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
customButton={
|
customButton={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-6 w-6 place-items-center rounded border ${
|
className={`grid place-items-center rounded ${
|
||||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
} items-center shadow-sm ${
|
} ${noBorder ? "" : "h-6 w-6 border shadow-sm"} ${
|
||||||
issue.priority === "urgent"
|
noBorder
|
||||||
|
? ""
|
||||||
|
: issue.priority === "urgent"
|
||||||
? "border-red-500/20 bg-red-500/20 text-red-500"
|
? "border-red-500/20 bg-red-500/20 text-red-500"
|
||||||
: issue.priority === "high"
|
: issue.priority === "high"
|
||||||
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
||||||
@ -67,14 +73,19 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
: issue.priority === "low"
|
: issue.priority === "low"
|
||||||
? "border-green-500/20 bg-green-500/20 text-green-500"
|
? "border-green-500/20 bg-green-500/20 text-green-500"
|
||||||
: "border-brand-base"
|
: "border-brand-base"
|
||||||
}`}
|
} items-center`}
|
||||||
>
|
>
|
||||||
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
|
||||||
<span>
|
<span className="flex gap-1 items-center text-brand-secondary text-xs">
|
||||||
{getPriorityIcon(
|
{getPriorityIcon(
|
||||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||||
"text-sm"
|
"text-sm"
|
||||||
)}
|
)}
|
||||||
|
{noBorder
|
||||||
|
? issue.priority && issue.priority !== ""
|
||||||
|
? capitalizeFirstLetter(issue.priority) ?? ""
|
||||||
|
: "None"
|
||||||
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
@ -22,6 +22,7 @@ type Props = {
|
|||||||
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
|
customButton?: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
@ -31,6 +32,7 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
partialUpdateIssue,
|
partialUpdateIssue,
|
||||||
position = "left",
|
position = "left",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
|
customButton = false,
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
}) => {
|
}) => {
|
||||||
@ -58,6 +60,19 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
|
|
||||||
const selectedOption = states?.find((s) => s.id === issue.state);
|
const selectedOption = states?.find((s) => s.id === issue.state);
|
||||||
|
|
||||||
|
const stateLabel = (
|
||||||
|
<Tooltip
|
||||||
|
tooltipHeading="State"
|
||||||
|
tooltipContent={addSpaceIfCamelCase(selectedOption?.name ?? "")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center cursor-pointer gap-2 text-brand-secondary">
|
||||||
|
{selectedOption &&
|
||||||
|
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
|
||||||
|
{selectedOption?.name ?? "State"}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={issue.state}
|
value={issue.state}
|
||||||
@ -101,18 +116,7 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
label={
|
{...(customButton ? { customButton: stateLabel } : { label: stateLabel })}
|
||||||
<Tooltip
|
|
||||||
tooltipHeading="State"
|
|
||||||
tooltipContent={addSpaceIfCamelCase(selectedOption?.name ?? "")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-brand-secondary">
|
|
||||||
{selectedOption &&
|
|
||||||
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
|
|
||||||
{selectedOption?.name ?? "State"}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
position={position}
|
position={position}
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
noChevron
|
noChevron
|
||||||
|
@ -20,6 +20,7 @@ type Props = {
|
|||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
verticalPosition?: "top" | "bottom";
|
verticalPosition?: "top" | "bottom";
|
||||||
customButton?: JSX.Element;
|
customButton?: JSX.Element;
|
||||||
|
menuItemsWhiteBg?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MenuItemProps = {
|
type MenuItemProps = {
|
||||||
@ -44,6 +45,7 @@ const CustomMenu = ({
|
|||||||
position = "right",
|
position = "right",
|
||||||
verticalPosition = "bottom",
|
verticalPosition = "bottom",
|
||||||
customButton,
|
customButton,
|
||||||
|
menuItemsWhiteBg = false,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<Menu as="div" className={`relative w-min whitespace-nowrap text-left ${className}`}>
|
<Menu as="div" className={`relative w-min whitespace-nowrap text-left ${className}`}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
@ -105,7 +107,7 @@ const CustomMenu = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className={`absolute z-20 overflow-y-scroll whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-1 p-1 text-xs shadow-lg focus:outline-none ${
|
className={`absolute z-20 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none ${
|
||||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||||
height === "sm"
|
height === "sm"
|
||||||
@ -127,6 +129,10 @@ const CustomMenu = ({
|
|||||||
: width === "xl"
|
: width === "xl"
|
||||||
? "w-48"
|
? "w-48"
|
||||||
: "min-w-full"
|
: "min-w-full"
|
||||||
|
} ${
|
||||||
|
menuItemsWhiteBg
|
||||||
|
? "border-brand-surface-1 bg-brand-base"
|
||||||
|
: "border-brand-base bg-brand-surface-1"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="py-1">{children}</div>
|
<div className="py-1">{children}</div>
|
||||||
|
@ -11,6 +11,7 @@ type Props = {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
displayShortForm?: boolean;
|
displayShortForm?: boolean;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
|
noBorder?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
isClearable?: boolean;
|
isClearable?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -23,6 +24,7 @@ export const CustomDatePicker: React.FC<Props> = ({
|
|||||||
placeholder = "Select date",
|
placeholder = "Select date",
|
||||||
displayShortForm = false,
|
displayShortForm = false,
|
||||||
error = false,
|
error = false,
|
||||||
|
noBorder = false,
|
||||||
className = "",
|
className = "",
|
||||||
isClearable = true,
|
isClearable = true,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@ -44,7 +46,9 @@ export const CustomDatePicker: React.FC<Props> = ({
|
|||||||
: ""
|
: ""
|
||||||
} ${error ? "border-red-500 bg-red-100" : ""} ${
|
} ${error ? "border-red-500 bg-red-100" : ""} ${
|
||||||
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
} w-full rounded-md border border-brand-base bg-transparent caret-transparent ${className}`}
|
} ${
|
||||||
|
noBorder ? "" : "border border-brand-base"
|
||||||
|
} w-full rounded-md bg-transparent caret-transparent ${className}`}
|
||||||
dateFormat="dd-MM-yyyy"
|
dateFormat="dd-MM-yyyy"
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -35,7 +35,7 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|||||||
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
<Menu as="div" className="relative z-30 inline-block text-left">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
|
@ -42,7 +42,7 @@ export const Tooltip: React.FC<Props> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
content={
|
content={
|
||||||
<div
|
<div
|
||||||
className={`${className} relative flex max-w-[600px] flex-col items-start justify-center gap-1 rounded-md p-2 text-left text-xs shadow-md ${
|
className={`${className} relative z-50 flex max-w-[600px] flex-col items-start justify-center gap-1 rounded-md p-2 text-left text-xs shadow-md ${
|
||||||
theme === "light" ? "text-brand-muted-1 bg-brand-surface-2" : "bg-black text-white"
|
theme === "light" ? "text-brand-muted-1 bg-brand-surface-2" : "bg-black text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { IAnalyticsParams, IJiraMetadata } from "types";
|
import { IAnalyticsParams, IJiraMetadata } from "types";
|
||||||
|
|
||||||
const paramsToKey = (params: any) => {
|
const paramsToKey = (params: any) => {
|
||||||
const { state, priority, assignees, created_by, labels, target_date } = params;
|
const { state, priority, assignees, created_by, labels, target_date, sub_issue } = params;
|
||||||
|
|
||||||
let stateKey = state ? state.split(",") : [];
|
let stateKey = state ? state.split(",") : [];
|
||||||
let priorityKey = priority ? priority.split(",") : [];
|
let priorityKey = priority ? priority.split(",") : [];
|
||||||
@ -12,6 +12,7 @@ const paramsToKey = (params: any) => {
|
|||||||
const type = params.type ? params.type.toUpperCase() : "NULL";
|
const type = params.type ? params.type.toUpperCase() : "NULL";
|
||||||
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
||||||
const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL";
|
const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL";
|
||||||
|
const subIssue = sub_issue ? sub_issue.toUpperCase() : "NULL";
|
||||||
|
|
||||||
// sorting each keys in ascending order
|
// sorting each keys in ascending order
|
||||||
stateKey = stateKey.sort().join("_");
|
stateKey = stateKey.sort().join("_");
|
||||||
@ -20,7 +21,7 @@ const paramsToKey = (params: any) => {
|
|||||||
createdByKey = createdByKey.sort().join("_");
|
createdByKey = createdByKey.sort().join("_");
|
||||||
labelsKey = labelsKey.sort().join("_");
|
labelsKey = labelsKey.sort().join("_");
|
||||||
|
|
||||||
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`;
|
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${subIssue}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const inboxParamsToKey = (params: any) => {
|
const inboxParamsToKey = (params: any) => {
|
||||||
|
60
apps/app/constants/spreadsheet.ts
Normal file
60
apps/app/constants/spreadsheet.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
CalendarDaysIcon,
|
||||||
|
PlayIcon,
|
||||||
|
Squares2X2Icon,
|
||||||
|
TagIcon,
|
||||||
|
UserGroupIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export const SPREADSHEET_COLUMN = [
|
||||||
|
{
|
||||||
|
propertyName: "title",
|
||||||
|
colName: "Title",
|
||||||
|
colSize: "440px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "state",
|
||||||
|
colName: "State",
|
||||||
|
colSize: "128px",
|
||||||
|
icon: Squares2X2Icon,
|
||||||
|
ascendingOrder: "state__name",
|
||||||
|
descendingOrder: "-state__name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "priority",
|
||||||
|
colName: "Priority",
|
||||||
|
colSize: "128px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "assignee",
|
||||||
|
colName: "Assignees",
|
||||||
|
colSize: "128px",
|
||||||
|
icon: UserGroupIcon,
|
||||||
|
ascendingOrder: "assignees__name",
|
||||||
|
descendingOrder: "-assignees__name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "labels",
|
||||||
|
colName: "Labels",
|
||||||
|
colSize: "128px",
|
||||||
|
icon: TagIcon,
|
||||||
|
ascendingOrder: "labels__name",
|
||||||
|
descendingOrder: "-labels__name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "due_date",
|
||||||
|
colName: "Due Date",
|
||||||
|
colSize: "128px",
|
||||||
|
icon: CalendarDaysIcon,
|
||||||
|
ascendingOrder: "target_date",
|
||||||
|
descendingOrder: "-target_date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "estimate",
|
||||||
|
colName: "Estimate",
|
||||||
|
colSize: "128px",
|
||||||
|
icon: PlayIcon,
|
||||||
|
ascendingOrder: "estimate_point",
|
||||||
|
descendingOrder: "-estimate_point",
|
||||||
|
},
|
||||||
|
];
|
125
apps/app/hooks/use-spreadsheet-issues-view.tsx
Normal file
125
apps/app/hooks/use-spreadsheet-issues-view.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// contexts
|
||||||
|
import { issueViewContext } from "contexts/issue-view.context";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
import cyclesService from "services/cycles.service";
|
||||||
|
import modulesService from "services/modules.service";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import {
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
VIEW_ISSUES,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const useSpreadsheetIssuesView = () => {
|
||||||
|
const {
|
||||||
|
issueView,
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
resetFilterToDefault,
|
||||||
|
setNewFilterDefaultView,
|
||||||
|
setIssueView,
|
||||||
|
} = useContext(issueViewContext);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
order_by: orderBy,
|
||||||
|
assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
|
||||||
|
state: filters?.state ? filters?.state.join(",") : undefined,
|
||||||
|
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||||
|
type: filters?.type ? filters?.type : undefined,
|
||||||
|
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
||||||
|
issue__assignees__id: filters?.issue__assignees__id
|
||||||
|
? filters?.issue__assignees__id.join(",")
|
||||||
|
: undefined,
|
||||||
|
issue__labels__id: filters?.issue__labels__id
|
||||||
|
? filters?.issue__labels__id.join(",")
|
||||||
|
: undefined,
|
||||||
|
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
||||||
|
sub_issue: "false",
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: projectSpreadsheetIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () =>
|
||||||
|
issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: cycleSpreadsheetIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId && cycleId
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && cycleId
|
||||||
|
? () =>
|
||||||
|
cyclesService.getCycleIssuesWithParams(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
cycleId.toString(),
|
||||||
|
params
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: moduleSpreadsheetIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId && moduleId
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && moduleId
|
||||||
|
? () =>
|
||||||
|
modulesService.getModuleIssuesWithParams(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
moduleId.toString(),
|
||||||
|
params
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: viewSpreadsheetIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null,
|
||||||
|
workspaceSlug && projectId && viewId && params
|
||||||
|
? () =>
|
||||||
|
issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const spreadsheetIssues = cycleId
|
||||||
|
? (cycleSpreadsheetIssues as IIssue[])
|
||||||
|
: moduleId
|
||||||
|
? (moduleSpreadsheetIssues as IIssue[])
|
||||||
|
: viewId
|
||||||
|
? (viewSpreadsheetIssues as IIssue[])
|
||||||
|
: (projectSpreadsheetIssues as IIssue[]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueView,
|
||||||
|
spreadsheetIssues: spreadsheetIssues ?? [],
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
params,
|
||||||
|
resetFilterToDefault,
|
||||||
|
setNewFilterDefaultView,
|
||||||
|
setIssueView,
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSpreadsheetIssuesView;
|
34
apps/app/hooks/use-sub-issue.tsx
Normal file
34
apps/app/hooks/use-sub-issue.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// types
|
||||||
|
import { ISubIssueResponse } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const useSubIssue = (issueId: string, isExpanded: boolean) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const shouldFetch = workspaceSlug && projectId && issueId && isExpanded;
|
||||||
|
|
||||||
|
const { data: subIssuesResponse, isLoading } = useSWR<ISubIssueResponse>(
|
||||||
|
shouldFetch ? SUB_ISSUES(issueId as string) : null,
|
||||||
|
shouldFetch
|
||||||
|
? () =>
|
||||||
|
issuesService.subIssues(workspaceSlug as string, projectId as string, issueId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subIssues: subIssuesResponse?.sub_issues ?? [],
|
||||||
|
isLoading,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSubIssue;
|
18
apps/app/types/issues.d.ts
vendored
18
apps/app/types/issues.d.ts
vendored
@ -247,11 +247,25 @@ export interface IIssueFilterOptions {
|
|||||||
created_by: string[] | null;
|
created_by: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "gantt_chart";
|
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
||||||
|
|
||||||
export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null;
|
export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null;
|
||||||
|
|
||||||
export type TIssueOrderByOptions = "-created_at" | "-updated_at" | "priority" | "sort_order";
|
export type TIssueOrderByOptions =
|
||||||
|
| "-created_at"
|
||||||
|
| "-updated_at"
|
||||||
|
| "priority"
|
||||||
|
| "sort_order"
|
||||||
|
| "state__name"
|
||||||
|
| "-state__name"
|
||||||
|
| "assignees__name"
|
||||||
|
| "-assignees__name"
|
||||||
|
| "labels__name"
|
||||||
|
| "-labels__name"
|
||||||
|
| "target_date"
|
||||||
|
| "-target_date"
|
||||||
|
| "estimate__point"
|
||||||
|
| "-estimate__point";
|
||||||
|
|
||||||
export interface IIssueViewOptions {
|
export interface IIssueViewOptions {
|
||||||
group_by: TIssueGroupByOptions;
|
group_by: TIssueGroupByOptions;
|
||||||
|
Loading…
Reference in New Issue
Block a user