chore: workspace view display filters and properties , code refactor (#2295)

* chore: spreadsheet view context

* chore: spreadsheet context provider

* chore: spreadsheet view context

* chore: display filters and properties added in workspace view and code refactor

* fix: build error fix

* chore: set sub-issue display option to false for global views

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
Anmol Singh Bhatia
2023-09-28 20:34:57 +05:30
committed by GitHub
parent 4503810aeb
commit a048e513b7
48 changed files with 3119 additions and 2369 deletions
+2 -59
View File
@@ -10,14 +10,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import {
IIssueFilterOptions,
IIssueLabels,
IProject,
IState,
IUserLite,
TStateGroups,
} from "types";
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@@ -27,9 +20,7 @@ type Props = {
clearAllFilters: (...args: any) => void;
labels: IIssueLabels[] | undefined;
members: IUserLite[] | undefined;
states?: IState[] | undefined;
stateGroup?: string[] | undefined;
project?: IProject[] | undefined;
states: IState[] | undefined;
};
export const FiltersList: React.FC<Props> = ({
@@ -39,7 +30,6 @@ export const FiltersList: React.FC<Props> = ({
labels,
members,
states,
project,
}) => {
if (!filters) return <></>;
@@ -165,29 +155,6 @@ export const FiltersList: React.FC<Props> = ({
: key === "assignees"
? filters.assignees?.map((memberId: string) => {
const member = members?.find((m) => m.id === memberId);
return (
<div
key={memberId}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
>
<Avatar user={member} />
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
assignees: filters.assignees?.filter((p: any) => p !== memberId),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "subscriber"
? filters.subscriber?.map((memberId: string) => {
const member = members?.find((m) => m.id === memberId);
return (
<div
@@ -333,30 +300,6 @@ export const FiltersList: React.FC<Props> = ({
</div>
);
})
: key === "project"
? filters.project?.map((projectId) => {
const currentProject = project?.find((p) => p.id === projectId);
console.log("currentProject", currentProject);
console.log("currentProject", projectId);
return (
<p
key={currentProject?.id}
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
>
<span>{currentProject?.name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
project: filters.project?.filter((p) => p !== projectId),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
);
})
: (filters[key] as any)?.join(", ")}
<button
type="button"
+1
View File
@@ -1,4 +1,5 @@
export * from "./date-filter-modal";
export * from "./date-filter-select";
export * from "./filters-list";
export * from "./workspace-filters-list";
export * from "./issues-view-filter";
@@ -0,0 +1,366 @@
import React from "react";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
import { PriorityIcon, StateGroupIcon } from "components/icons";
// ui
import { Avatar } from "components/ui";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import {
IIssueLabels,
IProject,
IUserLite,
IWorkspaceIssueFilterOptions,
TStateGroups,
} from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
filters: Partial<IWorkspaceIssueFilterOptions>;
setFilters: (updatedFilter: Partial<IWorkspaceIssueFilterOptions>) => void;
clearAllFilters: (...args: any) => void;
labels: IIssueLabels[] | undefined;
members: IUserLite[] | undefined;
stateGroup: string[] | undefined;
project?: IProject[] | undefined;
};
export const WorkspaceFiltersList: React.FC<Props> = ({
filters,
setFilters,
clearAllFilters,
labels,
members,
stateGroup,
project,
}) => {
if (!filters) return <></>;
const nullFilters = Object.keys(filters).filter(
(key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null
);
return (
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
{Object.keys(filters).map((filterKey) => {
const key = filterKey as keyof typeof filters;
if (filters[key] === null || (filters[key]?.length ?? 0) <= 0) return null;
return (
<div
key={key}
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
>
<span className="capitalize text-custom-text-200">
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
</span>
{filters[key] === null || (filters[key]?.length ?? 0) <= 0 ? (
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
) : Array.isArray(filters[key]) ? (
<div className="space-x-2">
<div className="flex flex-wrap items-center gap-1">
{key === "state_group"
? filters.state_group?.map((stateGroup) => {
const group = stateGroup as TStateGroups;
return (
<p
key={group}
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
style={{
color: STATE_GROUP_COLORS[group],
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
}}
>
<span>
<StateGroupIcon stateGroup={group} color={undefined} />
</span>
<span>{group}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
state_group: filters.state_group?.filter((g) => g !== group),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
);
})
: key === "priority"
? filters.priority?.map((priority: any) => (
<p
key={priority}
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
priority === "urgent"
? "bg-red-500/20 text-red-500"
: priority === "high"
? "bg-orange-500/20 text-orange-500"
: priority === "medium"
? "bg-yellow-500/20 text-yellow-500"
: priority === "low"
? "bg-green-500/20 text-green-500"
: "bg-custom-background-90 text-custom-text-200"
}`}
>
<span>
<PriorityIcon priority={priority} />
</span>
<span>{priority === "null" ? "None" : priority}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
priority: filters.priority?.filter((p: any) => p !== priority),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
))
: key === "assignees"
? filters.assignees?.map((memberId: string) => {
const member = members?.find((m) => m.id === memberId);
return (
<div
key={memberId}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
>
<Avatar user={member} />
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
assignees: filters.assignees?.filter((p: any) => p !== memberId),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "subscriber"
? filters.subscriber?.map((memberId: string) => {
const member = members?.find((m) => m.id === memberId);
return (
<div
key={memberId}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
>
<Avatar user={member} />
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
assignees: filters.assignees?.filter((p: any) => p !== memberId),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "created_by"
? filters.created_by?.map((memberId: string) => {
const member = members?.find((m) => m.id === memberId);
return (
<div
key={`${memberId}-${key}`}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
>
<Avatar user={member} />
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
created_by: filters.created_by?.filter(
(p: any) => p !== memberId
),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "labels"
? filters.labels?.map((labelId: string) => {
const label = labels?.find((l) => l.id === labelId);
if (!label) return null;
const color = label.color !== "" ? label.color : "#0f172a";
return (
<div
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
style={{
color: color,
backgroundColor: `${color}20`, // add 20% opacity
}}
key={labelId}
>
<div
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: color,
}}
/>
<span>{label.name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
labels: filters.labels?.filter((l: any) => l !== labelId),
})
}
>
<XMarkIcon
className="h-3 w-3"
style={{
color: color,
}}
/>
</span>
</div>
);
})
: key === "start_date"
? filters.start_date?.map((date: string) => {
if (filters.start_date && filters.start_date.length <= 0) return null;
const splitDate = date.split(";");
return (
<div
key={date}
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
>
<div className="h-1.5 w-1.5 rounded-full" />
<span className="capitalize">
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
start_date: filters.start_date?.filter((d: any) => d !== date),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "target_date"
? filters.target_date?.map((date: string) => {
if (filters.target_date && filters.target_date.length <= 0) return null;
const splitDate = date.split(";");
return (
<div
key={date}
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
>
<div className="h-1.5 w-1.5 rounded-full" />
<span className="capitalize">
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
target_date: filters.target_date?.filter((d: any) => d !== date),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "project"
? filters.project?.map((projectId) => {
const currentProject = project?.find((p) => p.id === projectId);
console.log("currentProject", currentProject);
console.log("currentProject", projectId);
return (
<p
key={currentProject?.id}
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
>
<span>{currentProject?.name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
project: filters.project?.filter((p) => p !== projectId),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
);
})
: (filters[key] as any)?.join(", ")}
<button
type="button"
onClick={() =>
setFilters({
[key]: null,
})
}
>
<XMarkIcon className="h-3 w-3" />
</button>
</div>
</div>
) : (
<div className="flex items-center gap-x-1 capitalize">
{filters[key as keyof typeof filters]}
<button
type="button"
onClick={() =>
setFilters({
[key]: null,
})
}
>
<XMarkIcon className="h-3 w-3" />
</button>
</div>
)}
</div>
);
})}
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
<button
type="button"
onClick={clearAllFilters}
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
>
<span>Clear all filters</span>
<XMarkIcon className="h-3 w-3" />
</button>
)}
</div>
);
};
@@ -483,7 +483,6 @@ export const IssuesView: React.FC<Props> = ({
<CreateUpdateViewModal
isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)}
viewType="project"
preLoadedData={createViewModal}
user={user}
/>
@@ -10,5 +10,4 @@ export * from "./state-column";
export * from "./updated-on-column";
export * from "./spreadsheet-view";
export * from "./issue-column/issue-column";
export * from "./spreadsheet-columns";
export * from "./issue-column/spreadsheet-issue-column";
@@ -82,10 +82,10 @@ export const IssueColumn: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<div className="group flex items-center w-[28rem] text-sm h-11 sticky top-0 bg-custom-background-100 truncate border-b border-r border-custom-border-100">
<div className="group flex items-center w-[28rem] text-sm h-11 sticky top-0 bg-custom-background-100 truncate border-b border-r border-custom-border-200">
<div
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center"
style={issue.parent ? { paddingLeft } : {}}
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center w-24"
style={issue.parent && nestingLevel !== 0 ? { paddingLeft } : {}}
>
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
{properties.key && (
@@ -1,274 +0,0 @@
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 { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
setDisplayFilters({ order_by: order });
setSelectedMenuItem(`${order}_${itemKey}`);
setActiveSortingProperty(order === "-created_at" ? "" : 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-custom-background-90 w-full ${
col.propertyName === "title"
? "sticky left-0 z-20 bg-custom-background-90 pl-24"
: ""
}`}
>
{col.propertyName === "title" ? (
<div
className={`flex items-center justify-start gap-1.5 cursor-default text-sm text-custom-text-200 text-current w-full py-2.5 px-2`}
>
{col.colName}
</div>
) : (
<CustomMenu
className="!w-full"
customButton={
<div
className={`relative group flex items-center justify-start gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
activeSortingProperty === col.propertyName ? "bg-custom-background-80" : ""
}`}
>
{activeSortingProperty === col.propertyName && (
<div className="absolute top-1 right-1.5">
<Icon
iconName="filter_list"
className="flex items-center justify-center h-3.5 w-3.5 rounded-full bg-custom-primary text-xs text-white"
/>
</div>
)}
{col.icon ? (
<col.icon
className={`text-custom-text-200 group-hover:text-custom-text-100 ${
col.propertyName === "estimate" ? "-rotate-90" : ""
}`}
aria-hidden="true"
height="14"
width="14"
/>
) : col.propertyName === "priority" ? (
<span className="text-sm material-symbols-rounded text-custom-text-200">
signal_cellular_alt
</span>
) : (
""
)}
{col.colName}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
width="xl"
>
<CustomMenu.MenuItem
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-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex gap-2 items-center">
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</span>
<span>A</span>
<Icon iconName="east" className="text-sm" />
<span>Z</span>
</>
) : col.propertyName === "due_date" ||
col.propertyName === "created_on" ||
col.propertyName === "updated_on" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</span>
<span>New</span>
<Icon iconName="east" className="text-sm" />
<span>Old</span>
</>
) : (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</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={`mt-0.5 ${
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
? "bg-custom-background-80"
: ""
}`}
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-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex gap-2 items-center">
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
/>
</span>
<span>Z</span>
<Icon iconName="east" className="text-sm" />
<span>A</span>
</>
) : col.propertyName === "due_date" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
/>
</span>
<span>Old</span>
<Icon iconName="east" className="text-sm" />
<span>New</span>
</>
) : (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
/>
</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>
{selectedMenuItem &&
selectedMenuItem !== "" &&
displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(col.propertyName) && (
<CustomMenu.MenuItem
className={`mt-0.5${
selectedMenuItem === `-created_at_${col.propertyName}`
? "bg-custom-background-80"
: ""
}`}
key={col.property}
onClick={() => {
handleOrderBy("-created_at", col.propertyName);
}}
>
<div className={`group flex gap-1.5 px-1 items-center justify-between `}>
<div className="flex gap-1.5 items-center">
<span className="relative flex items-center justify-center h-6 w-6">
<Icon iconName="ink_eraser" className="text-sm" />
</span>
<span>Clear sorting</span>
</div>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
);
}
})}
</div>
);
};
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
// next
import { useRouter } from "next/router";
@@ -19,13 +19,20 @@ import {
SpreadsheetStateColumn,
SpreadsheetUpdatedOnColumn,
} from "components/core";
import { CustomMenu, Spinner } from "components/ui";
import { CustomMenu, Icon, Spinner } from "components/ui";
import { IssuePeekOverview } from "components/issues";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useLocalStorage from "hooks/use-local-storage";
import { useWorkspaceView } from "hooks/use-workspace-view";
// types
import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types";
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
import {
ICurrentUserResponse,
IIssue,
ISubIssueResponse,
TIssueOrderByOptions,
UserAuth,
} from "types";
import {
CYCLE_DETAILS,
CYCLE_ISSUES_WITH_PARAMS,
@@ -39,7 +46,7 @@ import {
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import projectIssuesServices from "services/issues.service";
// icon
import { PlusIcon } from "lucide-react";
import { CheckIcon, ChevronDownIcon, PlusIcon } from "lucide-react";
type Props = {
spreadsheetIssues: IIssue[];
@@ -70,6 +77,10 @@ export const SpreadsheetView: React.FC<Props> = ({
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query;
@@ -77,6 +88,13 @@ export const SpreadsheetView: React.FC<Props> = ({
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
"spreadsheetViewSorting",
""
);
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
const workspaceIssuesPath = [
{
params: {
@@ -111,12 +129,9 @@ export const SpreadsheetView: React.FC<Props> = ({
router.pathname.includes(path.path)
);
const { params: workspaceViewParams } = useWorkspaceIssuesFilters(
workspaceSlug?.toString(),
workspaceViewId?.toString()
);
const { params: workspaceViewParams, handleFilters } = useWorkspaceView();
const { params } = useSpreadsheetIssuesView();
const { params, displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => {
@@ -199,8 +214,8 @@ export const SpreadsheetView: React.FC<Props> = ({
moduleId,
viewId,
workspaceViewId,
currentWorkspaceIssuePath,
workspaceViewParams,
currentWorkspaceIssuePath,
params,
user,
]
@@ -208,10 +223,216 @@ export const SpreadsheetView: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
const renderColumn = (header: string, Component: React.ComponentType<any>) => (
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm">
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
if (!workspaceViewId || !currentWorkspaceIssuePath)
handleFilters("display_filters", { order_by: order });
else setDisplayFilters({ order_by: order });
setSelectedMenuItem(`${order}_${itemKey}`);
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
};
const renderColumn = (
header: string,
propertyName: string,
Component: React.ComponentType<any>,
ascendingOrder: TIssueOrderByOptions,
descendingOrder: TIssueOrderByOptions
) => (
<div className="relative flex flex-col h-max w-full bg-custom-background-100">
<div className="flex items-center min-w-[9rem] px-4 py-2.5 text-sm font-medium z-[1] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200">
{header}
<CustomMenu
customButtonClassName="!w-full"
className="!w-full"
position="left"
customButton={
<div
className={`relative group flex items-center justify-between gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
activeSortingProperty === propertyName ? "bg-custom-background-80" : ""
}`}
>
{activeSortingProperty === propertyName && (
<div className="absolute top-1 right-1.5">
<Icon
iconName="filter_list"
className="flex items-center justify-center h-3.5 w-3.5 rounded-full bg-custom-primary text-xs text-white"
/>
</div>
)}
{header}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
width="xl"
>
<CustomMenu.MenuItem
onClick={() => {
handleOrderBy(ascendingOrder, propertyName);
}}
>
<div
className={`group flex gap-1.5 px-1 items-center justify-between ${
selectedMenuItem === `${ascendingOrder}_${propertyName}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex gap-2 items-center">
{propertyName === "assignee" || propertyName === "labels" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</span>
<span>A</span>
<Icon iconName="east" className="text-sm" />
<span>Z</span>
</>
) : propertyName === "due_date" ||
propertyName === "created_on" ||
propertyName === "updated_on" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</span>
<span>New</span>
<Icon iconName="east" className="text-sm" />
<span>Old</span>
</>
) : (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 rotate-90 text-xs leading-3"
/>
<Icon iconName="sort" className="absolute right-0 text-sm" />
</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 === `${ascendingOrder}_${propertyName}` ? "opacity-100" : ""
}`}
/>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
className={`mt-0.5 ${
selectedMenuItem === `${descendingOrder}_${propertyName}`
? "bg-custom-background-80"
: ""
}`}
key={propertyName}
onClick={() => {
handleOrderBy(descendingOrder, propertyName);
}}
>
<div
className={`group flex gap-1.5 px-1 items-center justify-between ${
selectedMenuItem === `${descendingOrder}_${propertyName}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex gap-2 items-center">
{propertyName === "assignee" || propertyName === "labels" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
/>
</span>
<span>Z</span>
<Icon iconName="east" className="text-sm" />
<span>A</span>
</>
) : propertyName === "due_date" ? (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
/>
</span>
<span>Old</span>
<Icon iconName="east" className="text-sm" />
<span>New</span>
</>
) : (
<>
<span className="relative flex items-center h-6 w-6">
<Icon
iconName="east"
className="absolute left-0 -rotate-90 text-xs leading-3"
/>
<Icon
iconName="sort"
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
/>
</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 === `${descendingOrder}_${propertyName}` ? "opacity-100" : ""
}`}
/>
</div>
</CustomMenu.MenuItem>
{selectedMenuItem &&
selectedMenuItem !== "" &&
displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(propertyName) && (
<CustomMenu.MenuItem
className={`mt-0.5${
selectedMenuItem === `-created_at_${propertyName}`
? "bg-custom-background-80"
: ""
}`}
key={propertyName}
onClick={() => {
handleOrderBy("-created_at", propertyName);
}}
>
<div className={`group flex gap-1.5 px-1 items-center justify-between `}>
<div className="flex gap-1.5 items-center">
<span className="relative flex items-center justify-center h-6 w-6">
<Icon iconName="ink_eraser" className="text-sm" />
</span>
<span>Clear sorting</span>
</div>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
</div>
<div className="h-full min-w-[9rem] w-full">
{spreadsheetIssues.map((issue: IIssue, index) => (
@@ -230,6 +451,27 @@ export const SpreadsheetView: React.FC<Props> = ({
</div>
);
const handleScroll = () => {
if (containerRef.current) {
const scrollLeft = containerRef.current.scrollLeft;
setIsScrolled(scrollLeft > 0);
}
};
useEffect(() => {
const currentContainerRef = containerRef.current;
if (currentContainerRef) {
currentContainerRef.addEventListener("scroll", handleScroll);
}
return () => {
if (currentContainerRef) {
currentContainerRef.removeEventListener("scroll", handleScroll);
}
};
}, []);
return (
<>
<IssuePeekOverview
@@ -238,15 +480,19 @@ export const SpreadsheetView: React.FC<Props> = ({
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-200">
<div className="h-full w-full flex flex-col">
<div className="flex max-h-full overflow-y-auto">
<div ref={containerRef} className="flex max-h-full h-full overflow-y-auto">
{spreadsheetIssues ? (
<>
<div className="sticky left-0 w-[28rem] z-[2]">
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm z-[2]">
<div
className={`relative flex flex-col h-max w-full bg-custom-background-100 z-[2] ${
isScrolled ? "shadow-r shadow-custom-shadow-xs" : ""
}`}
>
<div className="flex items-center text-sm font-medium z-[2] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200">
<span className="flex items-center px-4 py-2.5 h-full w-20 flex-shrink-0">
<span className="flex items-center px-4 py-2.5 h-full w-24 flex-shrink-0">
ID
</span>
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">
@@ -270,15 +516,69 @@ export const SpreadsheetView: React.FC<Props> = ({
))}
</div>
</div>
{renderColumn("State", SpreadsheetStateColumn)}
{renderColumn("Priority", SpreadsheetPriorityColumn)}
{renderColumn("Assignees", SpreadsheetAssigneeColumn)}
{renderColumn("Label", SpreadsheetLabelColumn)}
{renderColumn("Start Date", SpreadsheetStartDateColumn)}
{renderColumn("Due Date", SpreadsheetDueDateColumn)}
{renderColumn("Estimate", SpreadsheetEstimateColumn)}
{renderColumn("Created On", SpreadsheetCreatedOnColumn)}
{renderColumn("Updated On", SpreadsheetUpdatedOnColumn)}
{renderColumn(
"State",
"state",
SpreadsheetStateColumn,
"state__name",
"-state__name"
)}
{renderColumn(
"Priority",
"priority",
SpreadsheetPriorityColumn,
"priority",
"-priority"
)}
{renderColumn(
"Assignees",
"assignee",
SpreadsheetAssigneeColumn,
"assignees__first_name",
"-assignees__first_name"
)}
{renderColumn(
"Label",
"labels",
SpreadsheetLabelColumn,
"labels__name",
"-labels__name"
)}
{renderColumn(
"Start Date",
"start_date",
SpreadsheetStartDateColumn,
"-start_date",
"start_date"
)}
{renderColumn(
"Due Date",
"due_date",
SpreadsheetDueDateColumn,
"-target_date",
"target_date"
)}
{renderColumn(
"Estimate",
"estimate",
SpreadsheetEstimateColumn,
"estimate_point",
"-estimate_point"
)}
{renderColumn(
"Created On",
"created_on",
SpreadsheetCreatedOnColumn,
"-created_at",
"created_at"
)}
{renderColumn(
"Updated On",
"updated_on",
SpreadsheetUpdatedOnColumn,
"-updated_at",
"updated_at"
)}
</>
) : (
<div className="flex flex-col justify-center items-center h-full w-full">
@@ -76,7 +76,7 @@ export const StateColumn: React.FC<Props> = ({
value={issue.state_detail}
projectId={projectId}
onChange={handleStateChange}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
buttonClassName="!shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
/>