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:
Anmol Singh Bhatia 2023-06-23 17:20:05 +05:30 committed by GitHub
parent 0cb856b92f
commit e08fc59114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1093 additions and 69 deletions

View File

@ -2,6 +2,7 @@ export * from "./board-view";
export * from "./calendar-view";
export * from "./gantt-chart-view";
export * from "./list-view";
export * from "./spreadsheet-view";
export * from "./sidebar";
export * from "./bulk-delete-issues-modal";
export * from "./existing-issues-list-modal";

View File

@ -10,7 +10,7 @@ import { Popover, Transition } from "@headlessui/react";
// components
import { SelectFilters } from "components/views";
// ui
import { CustomMenu, ToggleSwitch } from "components/ui";
import { CustomMenu, Icon, ToggleSwitch } from "components/ui";
// icons
import {
ChevronDownIcon,
@ -83,6 +83,15 @@ export const IssuesFilterView: React.FC = () => {
>
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
</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
type="button"
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"
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="space-y-4 pb-3 text-xs">
{issueView !== "calendar" && (
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Group by</h4>
@ -221,7 +230,7 @@ export const IssuesFilterView: React.FC = () => {
</CustomMenu>
</div>
{issueView !== "calendar" && (
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-brand-secondary">Show empty states</h4>
@ -252,6 +261,13 @@ export const IssuesFilterView: React.FC = () => {
{Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
if (
(issueView === "spreadsheet" && key === "sub_issue_count") ||
key === "attachment_count" ||
key === "link"
)
return null;
return (
<button
key={key}

View File

@ -19,7 +19,14 @@ import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
import useUserAuth from "hooks/use-user-auth";
// 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 { CreateUpdateViewModal } from "components/views";
import { CycleIssuesGanttChartView, TransferIssues, TransferIssuesModal } from "components/cycles";
@ -563,6 +570,13 @@ export const IssuesView: React.FC<Props> = ({
user={user}
userAuth={memberRole}
/>
) : issueView === "spreadsheet" ? (
<SpreadsheetView
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
user={user}
userAuth={memberRole}
/>
) : (
issueView === "gantt_chart" && <GanttChartView />
)}

View File

@ -0,0 +1,4 @@
export * from "./spreadsheet-view";
export * from "./single-issue";
export * from "./spreadsheet-columns";
export * from "./spreadsheet-issues";

View 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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -12,6 +12,7 @@ import issueServices from "services/issues.service";
import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
@ -41,6 +42,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
const { issueView, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
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),
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 {
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));

View File

@ -17,6 +17,7 @@ import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast";
import useInboxView from "hooks/use-inbox-view";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
// components
import { IssueForm } from "components/issues";
// types
@ -79,6 +80,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params;
const { params: inboxParams } = useInboxView();
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId 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)
: 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
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId
@ -234,6 +244,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "gantt_chart") mutate(ganttFetchKey);
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
setToastAlert({
type: "success",
@ -264,6 +275,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}

View File

@ -22,6 +22,7 @@ type Props = {
position?: "left" | "right";
selfPositioned?: boolean;
tooltipPosition?: "left" | "right";
customButton?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@ -34,6 +35,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
tooltipPosition = "right",
user,
isNotAllowed,
customButton = false,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -65,32 +67,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
),
}));
return (
<CustomSearchSelect
value={issue.assignees}
onChange={(data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue.id);
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_ASSIGNEE",
user
);
}}
options={options}
label={
const assigneeLabel = (
<Tooltip
position={`top-${tooltipPosition}`}
tooltipHeading="Assignees"
@ -120,7 +97,34 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
)}
</div>
</Tooltip>
}
);
return (
<CustomSearchSelect
value={issue.assignees}
onChange={(data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue.id);
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_ASSIGNEE",
user
);
}}
options={options}
{...(customButton ? { customButton: assigneeLabel } : { label: assigneeLabel })}
multiple
noChevron
position={position}

View File

@ -12,6 +12,7 @@ import { ICurrentUserResponse, IIssue } from "types";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
noBorder?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@ -19,6 +20,7 @@ type Props = {
export const ViewDueDateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
noBorder = false,
user,
isNotAllowed,
}) => {
@ -62,6 +64,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
);
}}
className={issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}
noBorder={noBorder}
disabled={isNotAllowed}
/>
</div>

View File

@ -18,6 +18,7 @@ type Props = {
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
customButton?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@ -27,6 +28,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
partialUpdateIssue,
position = "left",
selfPositioned = false,
customButton = false,
user,
isNotAllowed,
}) => {
@ -37,6 +39,15 @@ export const ViewEstimateSelect: React.FC<Props> = ({
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;
return (
@ -57,14 +68,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
user
);
}}
label={
<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>
}
{...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })}
maxHeight="md"
noChevron
disabled={isNotAllowed}

View File

@ -12,12 +12,15 @@ import { ICurrentUserResponse, IIssue } from "types";
import { PRIORITIES } from "constants/project";
// services
import trackEventServices from "services/track-event.service";
// helper
import { capitalizeFirstLetter } from "helpers/string.helper";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
noBorder?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@ -27,6 +30,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
partialUpdateIssue,
position = "left",
selfPositioned = false,
noBorder = false,
user,
isNotAllowed,
}) => {
@ -55,10 +59,12 @@ export const ViewPrioritySelect: React.FC<Props> = ({
customButton={
<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"
} items-center shadow-sm ${
issue.priority === "urgent"
} ${noBorder ? "" : "h-6 w-6 border shadow-sm"} ${
noBorder
? ""
: issue.priority === "urgent"
? "border-red-500/20 bg-red-500/20 text-red-500"
: issue.priority === "high"
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
@ -67,14 +73,19 @@ export const ViewPrioritySelect: React.FC<Props> = ({
: issue.priority === "low"
? "border-green-500/20 bg-green-500/20 text-green-500"
: "border-brand-base"
}`}
} items-center`}
>
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
<span>
<span className="flex gap-1 items-center text-brand-secondary text-xs">
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
{noBorder
? issue.priority && issue.priority !== ""
? capitalizeFirstLetter(issue.priority) ?? ""
: "None"
: ""}
</span>
</Tooltip>
</button>

View File

@ -22,6 +22,7 @@ type Props = {
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
customButton?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@ -31,6 +32,7 @@ export const ViewStateSelect: React.FC<Props> = ({
partialUpdateIssue,
position = "left",
selfPositioned = false,
customButton = false,
user,
isNotAllowed,
}) => {
@ -58,6 +60,19 @@ export const ViewStateSelect: React.FC<Props> = ({
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 (
<CustomSearchSelect
value={issue.state}
@ -101,18 +116,7 @@ export const ViewStateSelect: React.FC<Props> = ({
}
}}
options={options}
label={
<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>
}
{...(customButton ? { customButton: stateLabel } : { label: stateLabel })}
position={position}
disabled={isNotAllowed}
noChevron

View File

@ -20,6 +20,7 @@ type Props = {
position?: "left" | "right";
verticalPosition?: "top" | "bottom";
customButton?: JSX.Element;
menuItemsWhiteBg?: boolean;
};
type MenuItemProps = {
@ -44,6 +45,7 @@ const CustomMenu = ({
position = "right",
verticalPosition = "bottom",
customButton,
menuItemsWhiteBg = false,
}: Props) => (
<Menu as="div" className={`relative w-min whitespace-nowrap text-left ${className}`}>
{({ open }) => (
@ -105,7 +107,7 @@ const CustomMenu = ({
leaveTo="transform opacity-0 scale-95"
>
<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"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
height === "sm"
@ -127,6 +129,10 @@ const CustomMenu = ({
: width === "xl"
? "w-48"
: "min-w-full"
} ${
menuItemsWhiteBg
? "border-brand-surface-1 bg-brand-base"
: "border-brand-base bg-brand-surface-1"
}`}
>
<div className="py-1">{children}</div>

View File

@ -11,6 +11,7 @@ type Props = {
placeholder?: string;
displayShortForm?: boolean;
error?: boolean;
noBorder?: boolean;
className?: string;
isClearable?: boolean;
disabled?: boolean;
@ -23,6 +24,7 @@ export const CustomDatePicker: React.FC<Props> = ({
placeholder = "Select date",
displayShortForm = false,
error = false,
noBorder = false,
className = "",
isClearable = true,
disabled = false,
@ -44,7 +46,9 @@ export const CustomDatePicker: React.FC<Props> = ({
: ""
} ${error ? "border-red-500 bg-red-100" : ""} ${
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"
isClearable={isClearable}
disabled={disabled}

View File

@ -35,7 +35,7 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
return (
<Menu as="div" className="relative z-10 inline-block text-left">
<Menu as="div" className="relative z-30 inline-block text-left">
{({ open }) => (
<>
<div>

View File

@ -42,7 +42,7 @@ export const Tooltip: React.FC<Props> = ({
disabled={disabled}
content={
<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"
}`}
>

View File

@ -1,7 +1,7 @@
import { IAnalyticsParams, IJiraMetadata } from "types";
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 priorityKey = priority ? priority.split(",") : [];
@ -12,6 +12,7 @@ const paramsToKey = (params: any) => {
const type = params.type ? params.type.toUpperCase() : "NULL";
const groupBy = params.group_by ? params.group_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
stateKey = stateKey.sort().join("_");
@ -20,7 +21,7 @@ const paramsToKey = (params: any) => {
createdByKey = createdByKey.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) => {

View 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",
},
];

View 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;

View 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;

View File

@ -247,11 +247,25 @@ export interface IIssueFilterOptions {
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 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 {
group_by: TIssueGroupByOptions;