refactor: spreadsheet layout

This commit is contained in:
Aaryan Khandelwal 2023-10-13 13:19:15 +05:30
parent 8aebf0bbd2
commit 0c13d05e27
12 changed files with 95 additions and 199 deletions

View File

@ -22,7 +22,6 @@ type Props = {
properties: Properties;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
disableUserActions: boolean;
nestingLevel: number;
};
@ -35,7 +34,6 @@ export const IssueColumn: React.FC<Props> = ({
properties,
handleEditIssue,
handleDeleteIssue,
setCurrentProjectId,
disableUserActions,
nestingLevel,
}) => {
@ -49,7 +47,7 @@ export const IssueColumn: React.FC<Props> = ({
const openPeekOverview = () => {
const { query } = router;
setCurrentProjectId(issue.project_detail.id);
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },

View File

@ -14,7 +14,6 @@ type Props = {
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
properties: Properties;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
disableUserActions: boolean;
nestingLevel?: number;
};
@ -26,7 +25,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
setExpandedIssues,
properties,
handleIssueAction,
setCurrentProjectId,
disableUserActions,
nestingLevel = 0,
}) => {
@ -57,7 +55,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
properties={properties}
handleEditIssue={() => handleIssueAction(issue, "edit")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
setCurrentProjectId={setCurrentProjectId}
disableUserActions={disableUserActions}
nestingLevel={nestingLevel}
/>
@ -75,7 +72,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
setExpandedIssues={setExpandedIssues}
properties={properties}
handleIssueAction={handleIssueAction}
setCurrentProjectId={setCurrentProjectId}
disableUserActions={disableUserActions}
nestingLevel={nestingLevel + 1}
/>

View File

@ -19,7 +19,7 @@ import { StateSelect } from "components/states";
import { copyTextToClipboard } from "helpers/string.helper";
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
// types
import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse, Properties, TIssuePriorities, UserAuth } from "types";
import { IUser, IIssue, IState, ISubIssueResponse, Properties, TIssuePriorities, UserAuth } from "types";
// constant
import {
CYCLE_DETAILS,
@ -42,7 +42,7 @@ type Props = {
handleDeleteIssue: (issue: IIssue) => void;
gridTemplateColumns: string;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
user: IUser | undefined;
userAuth: UserAuth;
nestingLevel: number;
};
@ -159,6 +159,8 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
if (!user) return;
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
@ -197,6 +199,8 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
};
const handlePriorityChange = (data: TIssuePriorities) => {
if (!user) return;
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
@ -213,6 +217,8 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
};
const handleAssigneeChange = (data: any) => {
if (!user) return;
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);

View File

@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite";
import useLocalStorage from "hooks/use-local-storage";
// components
import {
ListInlineCreateIssueForm,
// ListInlineCreateIssueForm,
SpreadsheetAssigneeColumn,
SpreadsheetCreatedOnColumn,
SpreadsheetDueDateColumn,
@ -19,7 +19,6 @@ import {
SpreadsheetUpdatedOnColumn,
} from "components/core";
import { CustomMenu, Icon } from "components/ui";
import { IssuePeekOverview } from "components/issues";
import { Spinner } from "@plane/ui";
// types
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "types";
@ -32,7 +31,7 @@ type Props = {
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
issues: IIssue[] | undefined;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleUpdateIssue: (issueId: string, data: Partial<IIssue>) => void;
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
openIssuesListModal?: (() => void) | null;
disableUserActions: boolean;
};
@ -50,7 +49,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
} = props;
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
@ -59,7 +57,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();
const { workspaceSlug, cycleId, moduleId } = router.query;
const { cycleId, moduleId } = router.query;
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
@ -266,10 +264,10 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
);
const handleScroll = () => {
if (containerRef.current) {
const scrollLeft = containerRef.current.scrollLeft;
setIsScrolled(scrollLeft > 0);
}
if (!containerRef.current) return;
const scrollLeft = containerRef.current.scrollLeft;
setIsScrolled(scrollLeft > 0);
};
useEffect(() => {
@ -288,11 +286,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
return (
<>
<IssuePeekOverview
projectId={currentProjectId ?? ""}
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-200">
<div className="h-full w-full flex flex-col">
<div ref={containerRef} className="flex max-h-full h-full overflow-y-auto">
@ -302,7 +295,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
<div
className="relative flex flex-col h-max w-full bg-custom-background-100 z-[2]"
style={{
boxShadow: isScrolled ? "8px -9px 12px rgba(0, 0, 0, 0.15)" : "",
boxShadow: isScrolled ? "4px -9px 12px rgba(0, 0, 0, 0.1)" : "",
}}
>
<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-100">
@ -312,14 +305,13 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">Issue</span>
</div>
{issues.map((issue: IIssue, index) => (
{issues.map((issue, index) => (
<SpreadsheetIssuesColumn
key={`${issue.id}_${index}`}
issue={issue}
projectId={issue.project_detail.id}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
setCurrentProjectId={setCurrentProjectId}
properties={displayProperties}
handleIssueAction={handleIssueAction}
disableUserActions={disableUserActions}
@ -361,7 +353,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
</div>
<div className="border-t border-custom-border-100">
<div className="mb-3 z-50 sticky bottom-0 left-0">
{/* <div className="mb-3 z-50 sticky bottom-0 left-0">
<ListInlineCreateIssueForm
isOpen={isInlineCreateIssueFormOpen}
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
@ -370,7 +362,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
...(moduleId && { module: moduleId.toString() }),
}}
/>
</div>
</div> */}
{type === "issue"
? !disableUserActions &&

View File

@ -1,5 +1,5 @@
export * from "./blocks";
export * from "./cycle-root";
export * from "./module-root";
export * from "./project-root";
export * from "./project-view-root";
export * from "./root";

View File

@ -11,7 +11,7 @@ import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "comp
// types
import { IIssueUnGroupedStructure } from "store/issue";
export const GanttLayout: React.FC = observer(() => {
export const ProjectGanttLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

View File

@ -11,6 +11,8 @@ import { observer } from "mobx-react-lite";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// types
import { IIssue } from "types";
export interface IGroupByKanBan {
issues: any;
@ -20,7 +22,7 @@ export interface IGroupByKanBan {
list: any;
listKey: string;
isDragDisabled: boolean;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
display_properties: any;
kanBanToggle: any;
handleKanBanToggle: any;

View File

@ -9,10 +9,10 @@ import { useMobxStore } from "lib/mobx/store-provider";
import {
ListLayout,
CalendarLayout,
GanttLayout,
ProjectGanttLayout,
KanBanLayout,
ProjectAppliedFiltersRoot,
SpreadsheetLayout,
ProjectSpreadsheetLayout,
} from "components/issues";
export const ProjectLayoutRoot: React.FC = observer(() => {
@ -26,6 +26,7 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
// TODO: remove fetch logic from here
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES` : null,
async () => {
@ -56,9 +57,9 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
<ProjectGanttLayout />
) : activeLayout === "spreadsheet" ? (
<SpreadsheetLayout />
<ProjectSpreadsheetLayout />
) : null}
</div>
</div>

View File

@ -1,4 +1,4 @@
export * from "./cycle-root";
export * from "./module-root";
export * from "./project-root";
export * from "./project-view-root";
export * from "./root";

View File

@ -0,0 +1,62 @@
import React, { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components
import { SpreadsheetView } from "components/core";
import { IssuePeekOverview } from "components/issues";
// types
import { IIssue, IIssueDisplayFilterOptions } from "types";
export const ProjectSpreadsheetLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { projectDetails } = useProjectDetails();
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore();
const issues = issueStore.getIssues;
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const updateIssue = (group_by: string | null, sub_group_by: string | null, issue: IIssue) => {
issueStore.updateIssueStructure(group_by, sub_group_by, issue);
};
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={!isAllowed}
/>
<SpreadsheetView
displayProperties={issueFilterStore.userDisplayProperties}
displayFilters={issueFilterStore.userDisplayFilters}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues as IIssue[]}
handleIssueAction={() => {}}
handleUpdateIssue={() => {}}
disableUserActions={false}
/>
</>
);
});

View File

@ -1,161 +0,0 @@
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useUser from "hooks/use-user";
import useProjectDetails from "hooks/use-project-details";
// components
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
import { IssuePeekOverview } from "components/issues";
// ui
import { CustomMenu } from "components/ui";
import { Spinner } from "@plane/ui";
// icon
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types";
import { IIssueUnGroupedStructure } from "store/issue";
// constants
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
export const SpreadsheetLayout: React.FC = observer(() => {
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore();
const issues = issueStore.getIssues;
const issueDisplayProperties = issueFilterStore.userDisplayProperties;
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const columnData = SPREADSHEET_COLUMN.map((column) => ({
...column,
isActive: issueDisplayProperties
? column.propertyName === "labels"
? issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
: column.propertyName === "title"
? true
: issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
: false,
}));
const gridTemplateColumns = columnData
.filter((column) => column.isActive)
.map((column) => column.colSize)
.join(" ");
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={!isAllowed}
/>
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
<SpreadsheetColumns
columnData={columnData}
displayFilters={issueFilterStore.userDisplayFilters}
gridTemplateColumns={gridTemplateColumns}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
/>
</div>
{issues ? (
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
{(issues as IIssueUnGroupedStructure).map((issue: IIssue, index) => (
<SpreadsheetIssues
key={`${issue.id}_${index}`}
index={index}
issue={issue}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns}
properties={issueDisplayProperties}
handleIssueAction={() => {}}
disableUserActions={!isAllowed}
user={user}
userAuth={{
isViewer: projectDetails?.member_role === 5,
isGuest: projectDetails?.member_role === 10,
isMember: projectDetails?.member_role === 15,
isOwner: projectDetails?.member_role === 20,
}}
/>
))}
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
{type === "issue" ? (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
isAllowed && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new
</CustomMenu.MenuItem>
{true && <CustomMenu.MenuItem onClick={() => {}}>Add an existing issue</CustomMenu.MenuItem>}
</CustomMenu>
)
)}
</div>
</div>
) : (
<Spinner />
)}
</div>
</>
);
});

View File

@ -99,13 +99,13 @@ class IssueStore implements IIssueStore {
return this.issues?.[projectId]?.[issueType] || null;
}
// TODO: params order is different from what is present in components
updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
const projectId: string | null = issue?.project;
const issueType = this.getIssueType;
if (!projectId || !issueType) return null;
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
this.getIssues;
let issues = this.getIssues;
if (!issues) return null;
if (issueType === "grouped" && group_id) {