chore: Refactor Spreadsheet view for better code maintainability and performance (#3322)

* refcator spreadsheet to use table and roow based approach rather than column based

* update spreadsheet and optimized layout

* fix issues in spread sheet

* close quick action menu on click

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
This commit is contained in:
rahulramesha 2024-01-11 18:19:19 +05:30 committed by sriram veeraghanta
parent 4611ec0b83
commit df97b35a99
35 changed files with 749 additions and 1274 deletions

View File

@ -36,6 +36,7 @@
"@headlessui/react": "^1.7.17",
"@popperjs/core": "^2.11.8",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-popper": "^2.3.0"
}
}

View File

@ -1,5 +1,5 @@
import * as React from "react";
import ReactDOM from "react-dom";
// react-poppper
import { usePopper } from "react-popper";
// hooks
@ -29,8 +29,10 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
optionsClassName = "",
verticalEllipsis = false,
width = "auto",
portalElement,
menuButtonOnClick,
tabIndex,
closeOnSelect,
} = props;
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
@ -51,6 +53,39 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
let menuItems = (
<Menu.Items
className="fixed z-10"
onClick={() => {
if (closeOnSelect) closeDropdown();
}}
static
>
<div
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
maxHeight === "lg"
? "max-h-60"
: maxHeight === "md"
? "max-h-48"
: maxHeight === "rg"
? "max-h-36"
: maxHeight === "sm"
? "max-h-28"
: ""
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{children}
</div>
</Menu.Items>
);
if (portalElement) {
menuItems = ReactDOM.createPortal(menuItems, portalElement);
}
return (
<Menu
as="div"
@ -118,28 +153,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
)}
</>
)}
{isOpen && (
<Menu.Items className="fixed z-10" static>
<div
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
maxHeight === "lg"
? "max-h-60"
: maxHeight === "md"
? "max-h-48"
: maxHeight === "rg"
? "max-h-36"
: maxHeight === "sm"
? "max-h-28"
: ""
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{children}
</div>
</Menu.Items>
)}
{isOpen && menuItems}
</>
)}
</Menu>

View File

@ -24,6 +24,8 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
noBorder?: boolean;
verticalEllipsis?: boolean;
menuButtonOnClick?: (...args: any) => void;
closeOnSelect?: boolean;
portalElement?: Element | null;
}
export interface ICustomSelectProps extends IDropdownProps {

View File

@ -4,4 +4,5 @@ export interface IQuickActionProps {
handleUpdate?: (data: TIssue) => Promise<void>;
handleRemoveFromView?: () => Promise<void>;
customActionButton?: React.ReactElement;
portalElement?: HTMLDivElement | null;
}

View File

@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
import { IQuickActionProps } from "../list/list-view-types";
export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
const { issue, handleDelete, handleUpdate, customActionButton } = props;
const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
@ -59,11 +59,15 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
<CustomMenu
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
closeOnSelect
ellipsis
>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
@ -74,8 +78,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
@ -87,8 +89,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
@ -99,8 +99,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>

View File

@ -12,7 +12,7 @@ import { copyUrlToClipboard } from "helpers/string.helper";
import { IQuickActionProps } from "../list/list-view-types";
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
const { issue, handleDelete, customActionButton } = props;
const { issue, handleDelete, customActionButton, portalElement } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
@ -40,11 +40,15 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete}
/>
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
<CustomMenu
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
closeOnSelect
ellipsis
>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
@ -55,8 +59,6 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>

View File

@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
import { IQuickActionProps } from "../list/list-view-types";
export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
@ -59,11 +59,15 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
<CustomMenu
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
closeOnSelect
ellipsis
>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
@ -74,8 +78,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit({
...issue,
cycle: cycleId?.toString() ?? null,
@ -90,8 +92,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromView && handleRemoveFromView();
}}
>
@ -102,8 +102,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
@ -114,8 +112,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>

View File

@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
import { IQuickActionProps } from "../list/list-view-types";
export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
@ -59,11 +59,15 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
<CustomMenu
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
closeOnSelect
ellipsis
>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
@ -74,8 +78,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
setCreateUpdateIssueModal(true);
}}
@ -87,8 +89,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromView && handleRemoveFromView();
}}
>
@ -99,8 +99,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>

View File

@ -16,7 +16,7 @@ import { IQuickActionProps } from "../list/list-view-types";
import { EUserProjectRoles } from "constants/project";
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
const { issue, handleDelete, handleUpdate, customActionButton } = props;
const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@ -68,11 +68,15 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
<CustomMenu
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
closeOnSelect
ellipsis
>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
@ -85,8 +89,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
<>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
@ -98,8 +100,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
@ -110,8 +110,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// hooks
import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
import { useGlobalView, useIssues, useUser } from "hooks/store";
// components
import { GlobalViewsAppliedFiltersRoot } from "components/issues";
import { SpreadsheetView } from "components/issues/issue-layouts";
@ -37,9 +37,6 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
const { fetchAllGlobalViews } = useGlobalView();
const {
workspace: { workspaceLabels },
} = useLabel();
// derived values
const currentIssueView = type ?? globalViewId;
@ -134,7 +131,6 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
/>
)}
labels={workspaceLabels || undefined}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
viewId={currentIssueView}

View File

@ -2,7 +2,7 @@ import { FC, useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useIssues, useLabel, useProjectState, useUser } from "hooks/store";
import { useIssues, useUser } from "hooks/store";
// views
import { SpreadsheetView } from "./spreadsheet-view";
// types
@ -40,10 +40,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
const {
membership: { currentProjectRole },
} = useUser();
const {
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState();
// derived values
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
// user role validation
@ -86,27 +82,31 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
[issueFiltersStore, projectId, workspaceSlug, viewId]
);
const renderQuickActions = useCallback(
(issue: TIssue, customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null) => (
<QuickActions
customActionButton={customActionButton}
issue={issue}
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
handleUpdate={
issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined
}
handleRemoveFromView={
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
}
portalElement={portalElement}
/>
),
[handleIssues]
);
return (
<SpreadsheetView
displayProperties={issueFiltersStore.issueFilters?.displayProperties ?? {}}
displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues}
quickActions={(issue, customActionButton) => (
<QuickActions
customActionButton={customActionButton}
issue={issue}
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
handleUpdate={
issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined
}
handleRemoveFromView={
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
}
/>
)}
labels={projectLabels ?? []}
states={projectStates}
quickActions={renderQuickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
quickAddCallback={issueStore.quickAddIssue}

View File

@ -1,56 +1,34 @@
import React from "react";
// hooks
import { useIssueDetail } from "hooks/store";
import { observer } from "mobx-react-lite";
// components
import { ProjectMemberDropdown } from "components/dropdowns";
// types
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
issue: TIssue;
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issueId) > -1;
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props) => {
const { issue, onChange, disabled } = props;
return (
<>
{issueDetail && (
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<ProjectMemberDropdown
value={issueDetail?.assignee_ids ?? []}
onChange={(data) => onChange(issueDetail, { assignee_ids: data })}
projectId={issueDetail?.project_id}
disabled={disabled}
multiple
placeholder="Assignees"
buttonVariant={issueDetail.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
buttonClassName="text-left"
buttonContainerClassName="w-full"
/>
</div>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId) => (
<SpreadsheetAssigneeColumn
key={subIssueId}
issueId={subIssueId}
onChange={onChange}
expandedIssues={expandedIssues}
disabled={disabled}
/>
))}
</>
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<ProjectMemberDropdown
value={issue?.assignee_ids ?? []}
onChange={(data) => onChange(issue, { assignee_ids: data })}
projectId={issue?.project_id}
disabled={disabled}
multiple
placeholder="Assignees"
buttonVariant={
issue?.assignee_ids && issue.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text"
}
buttonClassName="text-left"
buttonContainerClassName="w-full"
/>
</div>
);
};
});

View File

@ -1,39 +1,18 @@
import React from "react";
// hooks
import { observer } from "mobx-react-lite";
// types
import { useIssueDetail } from "hooks/store";
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
expandedIssues: string[];
issue: TIssue;
};
export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
const { issueId, expandedIssues } = props;
const isExpanded = expandedIssues.indexOf(issueId) > -1;
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
export const SpreadsheetAttachmentColumn: React.FC<Props> = observer((props) => {
const { issue } = props;
return (
<>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issueDetail?.attachment_count} {issueDetail?.attachment_count === 1 ? "attachment" : "attachments"}
</div>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className={`h-11`}>
<SpreadsheetAttachmentColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
</div>
))}
</>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"}
</div>
);
};
});

View File

@ -1,176 +0,0 @@
import { observer } from "mobx-react-lite";
// hooks
import { useProject } from "hooks/store";
// components
import { SpreadsheetColumn } from "components/issues";
// types
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types";
type Props = {
displayFilters: IIssueDisplayFilterOptions;
displayProperties: IIssueDisplayProperties;
canEditProperties: (projectId: string | undefined) => boolean;
expandedIssues: string[];
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
handleUpdateIssue: (issue: TIssue, data: Partial<TIssue>) => void;
issues: TIssue[] | undefined;
labels?: IIssueLabel[] | undefined;
states?: IState[] | undefined;
};
export const SpreadsheetColumnsList: React.FC<Props> = observer((props) => {
const {
canEditProperties,
displayFilters,
displayProperties,
expandedIssues,
handleDisplayFilterUpdate,
handleUpdateIssue,
issues,
labels,
states,
} = props;
// store hooks
const { currentProjectDetails } = useProject();
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
return (
<>
{displayProperties.state && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
states={states}
property="state"
/>
)}
{displayProperties.priority && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="priority"
/>
)}
{displayProperties.assignee && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="assignee"
/>
)}
{displayProperties.labels && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
labels={labels}
property="labels"
/>
)}{" "}
{displayProperties.start_date && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="start_date"
/>
)}
{displayProperties.due_date && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="due_date"
/>
)}
{displayProperties.estimate && isEstimateEnabled && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="estimate"
/>
)}
{displayProperties.created_on && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="created_on"
/>
)}
{displayProperties.updated_on && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="updated_on"
/>
)}
{displayProperties.link && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="link"
/>
)}
{displayProperties.attachment_count && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="attachment_count"
/>
)}
{displayProperties.sub_issue_count && (
<SpreadsheetColumn
displayFilters={displayFilters}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={handleUpdateIssue}
issues={issues}
property="sub_issue_count"
/>
)}
</>
);
});

View File

@ -1,38 +1,19 @@
import React from "react";
// hooks
import { useIssueDetail } from "hooks/store";
import { observer } from "mobx-react-lite";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
expandedIssues: string[];
issue: TIssue;
};
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issueId, expandedIssues }) => {
const isExpanded = expandedIssues.indexOf(issueId) > -1;
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
export const SpreadsheetCreatedOnColumn: React.FC<Props> = observer((props: Props) => {
const { issue } = props;
return (
<>
{issueDetail && (
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{renderFormattedDate(issueDetail.created_at)}
</div>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className="h-11">
<SpreadsheetCreatedOnColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
</div>
))}
</>
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{renderFormattedDate(issue.created_at)}
</div>
);
};
});

View File

@ -1,6 +1,5 @@
import React from "react";
// hooks
import { useIssueDetail } from "hooks/store";
import { observer } from "mobx-react-lite";
// components
import { DateDropdown } from "components/dropdowns";
// helpers
@ -9,49 +8,25 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
issue: TIssue;
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issueId) > -1;
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props) => {
const { issue, onChange, disabled } = props;
return (
<>
{issueDetail && (
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
value={issueDetail.target_date}
onChange={(data) => onChange(issueDetail, { target_date: data ? renderFormattedPayloadDate(data) : null })}
disabled={disabled}
placeholder="Due date"
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId) => (
<SpreadsheetDueDateColumn
key={subIssueId}
issueId={subIssueId}
onChange={onChange}
expandedIssues={expandedIssues}
disabled={disabled}
/>
))}
</>
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
value={issue.target_date}
onChange={(data) => onChange(issue, { target_date: data ? renderFormattedPayloadDate(data) : null })}
disabled={disabled}
placeholder="Due date"
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
);
};
});

View File

@ -1,56 +1,29 @@
// hooks
import { useIssueDetail } from "hooks/store";
// components
import { EstimateDropdown } from "components/dropdowns";
import { observer } from "mobx-react-lite";
// types
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
expandedIssues: string[];
issue: TIssue;
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
disabled: boolean;
};
export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
const { issueId, onChange, expandedIssues, disabled } = props;
const isExpanded = expandedIssues.indexOf(issueId) > -1;
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props) => {
const { issue, onChange, disabled } = props;
return (
<>
{issueDetail && (
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<EstimateDropdown
value={issueDetail.estimate_point}
onChange={(data) => onChange(issueDetail, { estimate_point: data })}
projectId={issueDetail.project_id}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId) => (
<SpreadsheetEstimateColumn
key={subIssueId}
issueId={subIssueId}
onChange={onChange}
expandedIssues={expandedIssues}
disabled={disabled}
/>
))}
</>
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<EstimateDropdown
value={issue.estimate_point}
onChange={(data) => onChange(issue, { estimate_point: data })}
projectId={issue.project_id}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
);
};
});

View File

@ -0,0 +1,123 @@
//ui
import { CustomMenu } from "@plane/ui";
import {
ArrowDownWideNarrow,
ArrowUpNarrowWide,
CheckIcon,
ChevronDownIcon,
Eraser,
ListFilter,
MoveRight,
} from "lucide-react";
//hooks
import useLocalStorage from "hooks/use-local-storage";
//types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types";
//constants
import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
interface Props {
property: keyof IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
}
export const SpreadsheetHeaderColumn = (props: Props) => {
const { displayFilters, handleDisplayFilterUpdate, property } = props;
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
"spreadsheetViewSorting",
""
);
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage(
"spreadsheetViewActiveSortingProperty",
""
);
const propertyDetails = SPREADSHEET_PROPERTY_DETAILS[property];
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
handleDisplayFilterUpdate({ order_by: order });
setSelectedMenuItem(`${order}_${itemKey}`);
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
};
return (
<CustomMenu
customButtonClassName="!w-full"
className="!w-full"
customButton={
<div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
<div className="flex items-center gap-1.5">
{<propertyDetails.icon className="h-4 w-4 text-custom-text-400" />}
{propertyDetails.title}
</div>
<div className="ml-3 flex">
{activeSortingProperty === property && (
<div className="flex h-3.5 w-3.5 items-center justify-center rounded-full">
<ListFilter className="h-3 w-3" />
</div>
)}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
</div>
}
width="xl"
placement="bottom-end"
>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.ascendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.descendingOrderTitle}</span>
</div>
{selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && <CheckIcon className="h-3 w-3" />}
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.descendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.ascendingOrderTitle}</span>
</div>
{selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && (
<CheckIcon className="h-3 w-3" />
)}
</div>
</CustomMenu.MenuItem>
{selectedMenuItem &&
selectedMenuItem !== "" &&
displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(property) && (
<CustomMenu.MenuItem
className={`mt-0.5 ${selectedMenuItem === `-created_at_${property}` ? "bg-custom-background-80" : ""}`}
key={property}
onClick={() => handleOrderBy("-created_at", property)}
>
<div className="flex items-center gap-2 px-1">
<Eraser className="h-3 w-3" />
<span>Clear sorting</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
);
};

View File

@ -1,7 +1,5 @@
export * from "./issue";
export * from "./assignee-column";
export * from "./attachment-column";
export * from "./columns-list";
export * from "./created-on-column";
export * from "./due-date-column";
export * from "./estimate-column";
@ -11,4 +9,4 @@ export * from "./priority-column";
export * from "./start-date-column";
export * from "./state-column";
export * from "./sub-issue-column";
export * from "./updated-on-column";
export * from "./updated-on-column";

View File

@ -1,2 +0,0 @@
export * from "./spreadsheet-issue-column";
export * from "./issue-column";

View File

@ -1,114 +0,0 @@
import React, { useRef, useState } from "react";
import { useRouter } from "next/router";
import { ChevronRight, MoreHorizontal } from "lucide-react";
// components
import { Tooltip } from "@plane/ui";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// types
import { TIssue, IIssueDisplayProperties } from "@plane/types";
import { useProject } from "hooks/store";
type Props = {
issue: TIssue;
expanded: boolean;
handleToggleExpand: (issueId: string) => void;
properties: IIssueDisplayProperties;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
nestingLevel: number;
};
export const IssueColumn: React.FC<Props> = ({
issue,
expanded,
handleToggleExpand,
properties,
quickActions,
canEditProperties,
nestingLevel,
}) => {
// router
const router = useRouter();
// hooks
const { getProjectById } = useProject();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const menuActionRef = useRef<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: TIssue) => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
});
};
const paddingLeft = `${nestingLevel * 54}px`;
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
const customActionButton = (
<div
ref={menuActionRef}
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
}`}
onClick={() => setIsMenuActive(!isMenuActive)}
>
<MoreHorizontal className="h-3.5 w-3.5" />
</div>
);
return (
<>
<div className="group top-0 flex h-11 w-[28rem] items-center truncate border-b border-custom-border-100 bg-custom-background-100 text-sm">
{properties.key && (
<div
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
style={issue.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
>
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-custom-text-100">
<span
className={`flex items-center justify-center font-medium opacity-100 group-hover:opacity-0 ${
isMenuActive ? "!opacity-0" : ""
} `}
>
{getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
</span>
{canEditProperties(issue.project_id) && (
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
{quickActions(issue, customActionButton)}
</div>
)}
</div>
{issue.sub_issues_count > 0 && (
<div className="flex h-6 w-6 items-center justify-center">
<button
className="h-5 w-5 cursor-pointer rounded-sm hover:bg-custom-background-90 hover:text-custom-text-100"
onClick={() => handleToggleExpand(issue.id)}
>
<ChevronRight className={`h-3.5 w-3.5 ${expanded ? "rotate-90" : ""}`} />
</button>
</div>
)}
</div>
)}
<div className="w-full overflow-hidden">
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
onClick={() => handleIssuePeekOverview(issue)}
>
{issue.name}
</div>
</Tooltip>
</div>
</div>
</>
);
};

View File

@ -1,81 +0,0 @@
import React from "react";
// components
import { IssueColumn } from "components/issues";
// hooks
import { useIssueDetail } from "hooks/store";
// types
import { TIssue, IIssueDisplayProperties } from "@plane/types";
type Props = {
issueId: string;
expandedIssues: string[];
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
properties: IIssueDisplayProperties;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
nestingLevel?: number;
};
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
issueId,
expandedIssues,
setExpandedIssues,
properties,
quickActions,
canEditProperties,
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(issueId) > -1;
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
return (
<>
{issueDetail && (
<IssueColumn
issue={issueDetail}
expanded={isExpanded}
handleToggleExpand={handleToggleExpand}
properties={properties}
canEditProperties={canEditProperties}
nestingLevel={nestingLevel}
quickActions={quickActions}
/>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<SpreadsheetIssuesColumn
key={subIssueId}
issueId={subIssueId}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
properties={properties}
quickActions={quickActions}
canEditProperties={canEditProperties}
nestingLevel={nestingLevel + 1}
/>
))}
</>
);
};

View File

@ -1,70 +1,39 @@
import React from "react";
import { observer } from "mobx-react-lite";
// components
import { IssuePropertyLabels } from "../../properties";
// hooks
import { useIssueDetail, useLabel } from "hooks/store";
import { useLabel } from "hooks/store";
// types
import { TIssue, IIssueLabel } from "@plane/types";
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
labels: IIssueLabel[] | undefined;
expandedIssues: string[];
issue: TIssue;
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
disabled: boolean;
};
export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
const { issueId, onChange, labels, expandedIssues, disabled } = props;
export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) => {
const { issue, onChange, disabled } = props;
// hooks
const { labelMap } = useLabel();
const isExpanded = expandedIssues.indexOf(issueId) > -1;
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
const defaultLabelOptions = issueDetail?.label_ids?.map((id) => labelMap[id]) || [];
const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
return (
<>
{issueDetail && (
<IssuePropertyLabels
projectId={issueDetail.project_id ?? null}
value={issueDetail.label_ids}
defaultOptions={defaultLabelOptions}
onChange={(data) => {
onChange(issueDetail, { label_ids: data });
}}
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
buttonClassName="px-2.5 h-full"
hideDropdownArrow
maxRender={1}
disabled={disabled}
placeholderText="Select labels"
/>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className={`h-11`}>
<SpreadsheetLabelColumn
key={subIssueId}
issueId={subIssueId}
onChange={onChange}
labels={labels}
expandedIssues={expandedIssues}
disabled={disabled}
/>
</div>
))}
</>
<IssuePropertyLabels
projectId={issue.project_id ?? null}
value={issue.label_ids}
defaultOptions={defaultLabelOptions}
onChange={(data) => {
onChange(issue, { label_ids: data });
}}
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
buttonClassName="px-2.5 h-full"
hideDropdownArrow
maxRender={1}
disabled={disabled}
placeholderText="Select labels"
/>
);
};
});

View File

@ -1,39 +1,18 @@
import React from "react";
// hooks
import { useIssueDetail } from "hooks/store";
import { observer } from "mobx-react-lite";
// types
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
expandedIssues: string[];
issue: TIssue;
};
export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
const { issueId, expandedIssues } = props;
const isExpanded = expandedIssues.indexOf(issueId) > -1;
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
export const SpreadsheetLinkColumn: React.FC<Props> = observer((props: Props) => {
const { issue } = props;
return (
<>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issueDetail?.link_count} {issueDetail?.link_count === 1 ? "link" : "links"}
</div>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className={`h-11`}>
<SpreadsheetLinkColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
</div>
))}
</>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issue?.link_count} {issue?.link_count === 1 ? "link" : "links"}
</div>
);
};
});

View File

@ -1,54 +1,29 @@
import React from "react";
// hooks
import { useIssueDetail } from "hooks/store";
import { observer } from "mobx-react-lite";
// components
import { PriorityDropdown } from "components/dropdowns";
// types
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
issue: TIssue;
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
export const SpreadsheetPriorityColumn: React.FC<Props> = (props) => {
const { issueId, onChange, expandedIssues, disabled } = props;
// store hooks
const { subIssues: subIssuesStore, issue } = useIssueDetail();
// derived values
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
const isExpanded = expandedIssues.indexOf(issueId) > -1;
export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props) => {
const { issue, onChange, disabled } = props;
return (
<>
{issueDetail && (
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<PriorityDropdown
value={issueDetail.priority}
onChange={(data) => onChange(issueDetail, { priority: data })}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<SpreadsheetPriorityColumn
key={subIssueId}
issueId={subIssueId}
onChange={onChange}
expandedIssues={expandedIssues}
disabled={disabled}
/>
))}
</>
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<PriorityDropdown
value={issue.priority}
onChange={(data) => onChange(issue, { priority: data })}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
);
};
});

View File

@ -1,6 +1,5 @@
import React from "react";
// hooks
import { useIssueDetail } from "hooks/store";
import { observer } from "mobx-react-lite";
// components
import { DateDropdown } from "components/dropdowns";
// helpers
@ -9,50 +8,25 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
expandedIssues: string[];
issue: TIssue;
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
disabled: boolean;
};
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issueId) > -1;
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Props) => {
const { issue, onChange, disabled } = props;
return (
<>
{issueDetail && (
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
value={issueDetail.start_date}
onChange={(data) => onChange(issueDetail, { start_date: data ? renderFormattedPayloadDate(data) : null })}
disabled={disabled}
placeholder="Start date"
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId) => (
<SpreadsheetStartDateColumn
key={subIssueId}
issueId={subIssueId}
onChange={onChange}
expandedIssues={expandedIssues}
disabled={disabled}
/>
))}
</>
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
value={issue.start_date}
onChange={(data) => onChange(issue, { start_date: data ? renderFormattedPayloadDate(data) : null })}
disabled={disabled}
placeholder="Start date"
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
);
};
});

View File

@ -1,59 +1,30 @@
import React from "react";
// hooks
import { useIssueDetail } from "hooks/store";
import { observer } from "mobx-react-lite";
// components
import { StateDropdown } from "components/dropdowns";
// types
import { TIssue, IState } from "@plane/types";
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
issue: TIssue;
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
states: IState[] | undefined;
expandedIssues: string[];
disabled: boolean;
};
export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
const { issueId, onChange, states, expandedIssues, disabled } = props;
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
const isExpanded = expandedIssues.indexOf(issueId) > -1;
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
const { issue, onChange, disabled } = props;
return (
<>
{issueDetail && (
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<StateDropdown
projectId={issueDetail.project_id}
value={issueDetail.state_id}
onChange={(data) => onChange(issueDetail, { state_id: data })}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId) => (
<SpreadsheetStateColumn
key={subIssueId}
issueId={subIssueId}
onChange={onChange}
states={states}
expandedIssues={expandedIssues}
disabled={disabled}
/>
))}
</>
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<StateDropdown
projectId={issue.project_id}
value={issue.state_id}
onChange={(data) => onChange(issue, { state_id: data })}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
/>
</div>
);
};
});

View File

@ -1,37 +1,18 @@
import React from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useIssueDetail } from "hooks/store";
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
expandedIssues: string[];
issue: TIssue;
};
export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
const { issueId, expandedIssues } = props;
const isExpanded = expandedIssues.indexOf(issueId) > -1;
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
export const SpreadsheetSubIssueColumn: React.FC<Props> = observer((props: Props) => {
const { issue } = props;
return (
<>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issueDetail?.sub_issues_count} {issueDetail?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className={`h-11`}>
<SpreadsheetSubIssueColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
</div>
))}
</>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
);
};
});

View File

@ -1,43 +1,19 @@
import React from "react";
// hooks
// import useSubIssue from "hooks/use-sub-issue";
import { observer } from "mobx-react-lite";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { useIssueDetail } from "hooks/store";
import { TIssue } from "@plane/types";
type Props = {
issueId: string;
expandedIssues: string[];
issue: TIssue;
};
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
const { issueId, expandedIssues } = props;
const isExpanded = expandedIssues.indexOf(issueId) > -1;
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = observer((props: Props) => {
const { issue } = props;
return (
<>
{issueDetail && (
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{renderFormattedDate(issueDetail.updated_at)}
</div>
)}
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className={`h-11`}>
<SpreadsheetUpdatedOnColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
</div>
))}
</>
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{renderFormattedDate(issue.updated_at)}
</div>
);
};
});

View File

@ -1,5 +1,4 @@
export * from "./columns";
export * from "./roots";
export * from "./spreadsheet-column";
export * from "./spreadsheet-view";
export * from "./quick-add-issue-form";

View File

@ -0,0 +1,186 @@
import { IIssueDisplayProperties, TIssue, TIssueMap } from "@plane/types";
import { SPREADSHEET_PROPERTY_DETAILS, SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { Tooltip } from "@plane/ui";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { useIssueDetail, useProject } from "hooks/store";
import { useRef, useState } from "react";
import { useRouter } from "next/router";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { EIssueActions } from "../types";
import { observer } from "mobx-react-lite";
interface Props {
displayProperties: IIssueDisplayProperties;
isEstimateEnabled: boolean;
quickActions: (
issue: TIssue,
customActionButton?: React.ReactElement,
portalElement?: HTMLDivElement | null
) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
portalElement: React.MutableRefObject<HTMLDivElement | null>;
nestingLevel: number;
issueId: string;
}
export const SpreadsheetIssueRow = observer((props: Props) => {
const {
displayProperties,
issueId,
isEstimateEnabled,
nestingLevel,
portalElement,
handleIssues,
quickActions,
canEditProperties,
} = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { getProjectById } = useProject();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const [isExpanded, setExpanded] = useState<boolean>(false);
const menuActionRef = useRef<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: TIssue) => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
});
};
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
const paddingLeft = `${nestingLevel * 54}px`;
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
const handleToggleExpand = () => {
setExpanded((prevState) => {
if (!prevState && workspaceSlug && issueDetail)
subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issueDetail.project_id, issueDetail.id);
return !prevState;
});
};
const customActionButton = (
<div
ref={menuActionRef}
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
}`}
onClick={() => setIsMenuActive(!isMenuActive)}
>
<MoreHorizontal className="h-3.5 w-3.5" />
</div>
);
if (!issueDetail) return null;
const disableUserActions = !canEditProperties(issueDetail.project_id);
return (
<>
<tr>
{/* first column/ issue name and key column */}
<td className="sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-l-0 after:border-custom-border-100 before:absolute before:h-full before:right-0 before:border before:border-l-0 before:border-custom-border-100">
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
<div
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
>
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-custom-text-100">
<span
className={`flex items-center justify-center font-medium group-hover:opacity-0 ${
isMenuActive ? "opacity-0" : "opacity-100"
}`}
>
{getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
</span>
{canEditProperties(issueDetail.project_id) && (
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
{quickActions(issueDetail, customActionButton, portalElement.current)}
</div>
)}
</div>
{issueDetail.sub_issues_count > 0 && (
<div className="flex h-6 w-6 items-center justify-center">
<button
className="h-5 w-5 cursor-pointer rounded-sm hover:bg-custom-background-90 hover:text-custom-text-100"
onClick={() => handleToggleExpand()}
>
<ChevronRight className={`h-3.5 w-3.5 ${isExpanded ? "rotate-90" : ""}`} />
</button>
</div>
)}
</div>
</WithDisplayPropertiesHOC>
<div className="w-full overflow-hidden">
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
<div
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
onClick={() => handleIssuePeekOverview(issueDetail)}
>
{issueDetail.name}
</div>
</Tooltip>
</div>
</td>
{/* Rest of the columns */}
{SPREADSHEET_PROPERTY_LIST.map((property) => {
const { Column } = SPREADSHEET_PROPERTY_DETAILS[property];
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
return (
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey={property}
shouldRenderProperty={shouldRenderProperty}
>
<td className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100">
<Column
issue={issueDetail}
onChange={(issue: TIssue, data: Partial<TIssue>) =>
handleIssues({ ...issue, ...data }, EIssueActions.UPDATE)
}
disabled={disableUserActions}
/>
</td>
</WithDisplayPropertiesHOC>
);
})}
</tr>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<SpreadsheetIssueRow
key={subIssueId}
issueId={subIssueId}
displayProperties={displayProperties}
quickActions={quickActions}
canEditProperties={canEditProperties}
nestingLevel={nestingLevel + 1}
isEstimateEnabled={isEstimateEnabled}
handleIssues={handleIssues}
portalElement={portalElement}
/>
))}
</>
);
});

View File

@ -1,233 +0,0 @@
import {
ArrowDownWideNarrow,
ArrowUpNarrowWide,
CheckIcon,
ChevronDownIcon,
Eraser,
ListFilter,
MoveRight,
} from "lucide-react";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components
import {
SpreadsheetAssigneeColumn,
SpreadsheetAttachmentColumn,
SpreadsheetCreatedOnColumn,
SpreadsheetDueDateColumn,
SpreadsheetEstimateColumn,
SpreadsheetLabelColumn,
SpreadsheetLinkColumn,
SpreadsheetPriorityColumn,
SpreadsheetStartDateColumn,
SpreadsheetStateColumn,
SpreadsheetSubIssueColumn,
SpreadsheetUpdatedOnColumn,
} from "components/issues";
// ui
import { CustomMenu } from "@plane/ui";
// types
import { TIssue, IIssueDisplayFilterOptions, IIssueLabel, IState, TIssueOrderByOptions } from "@plane/types";
// constants
import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
type Props = {
canEditProperties: (projectId: string | undefined) => boolean;
displayFilters: IIssueDisplayFilterOptions;
expandedIssues: string[];
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
handleUpdateIssue: (issue: TIssue, data: Partial<TIssue>) => void;
issues: TIssue[] | undefined;
property: string;
labels?: IIssueLabel[] | undefined;
states?: IState[] | undefined;
};
export const SpreadsheetColumn: React.FC<Props> = (props) => {
const {
canEditProperties,
displayFilters,
expandedIssues,
handleDisplayFilterUpdate,
handleUpdateIssue,
issues,
property,
labels,
states,
} = props;
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
"spreadsheetViewSorting",
""
);
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage(
"spreadsheetViewActiveSortingProperty",
""
);
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
handleDisplayFilterUpdate({ order_by: order });
setSelectedMenuItem(`${order}_${itemKey}`);
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
};
const propertyDetails = SPREADSHEET_PROPERTY_DETAILS[property];
return (
<div className="flex h-max w-full max-w-max flex-col bg-custom-background-100">
<div className="sticky top-0 z-[1] flex h-11 w-full min-w-[8rem] items-center border border-l-0 border-custom-border-100 bg-custom-background-90 px-4 py-1 text-sm font-medium">
<CustomMenu
customButtonClassName="!w-full"
className="!w-full"
customButton={
<div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
<div className="flex items-center gap-1.5">
{<propertyDetails.icon className="h-4 w-4 text-custom-text-400" />}
{propertyDetails.title}
</div>
<div className="ml-3 flex">
{activeSortingProperty === property && (
<div className="flex h-3.5 w-3.5 items-center justify-center rounded-full">
<ListFilter className="h-3 w-3" />
</div>
)}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
</div>
}
width="xl"
placement="bottom-end"
>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.ascendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.descendingOrderTitle}</span>
</div>
{selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && (
<CheckIcon className="h-3 w-3" />
)}
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.descendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.ascendingOrderTitle}</span>
</div>
{selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && (
<CheckIcon className="h-3 w-3" />
)}
</div>
</CustomMenu.MenuItem>
{selectedMenuItem &&
selectedMenuItem !== "" &&
displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(property) && (
<CustomMenu.MenuItem
className={`mt-0.5 ${selectedMenuItem === `-created_at_${property}` ? "bg-custom-background-80" : ""}`}
key={property}
onClick={() => handleOrderBy("-created_at", property)}
>
<div className="flex items-center gap-2 px-1">
<Eraser className="h-3 w-3" />
<span>Clear sorting</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
</div>
<div className="h-full w-full min-w-[8rem]">
{issues?.map((issue) => {
const disableUserActions = !canEditProperties(issue.project_id);
return (
<div key={`${property}-${issue.id}`} className={`h-fit ${disableUserActions ? "" : "cursor-pointer"}`}>
{property === "state" ? (
<SpreadsheetStateColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issueId={issue.id}
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
states={states}
/>
) : property === "priority" ? (
<SpreadsheetPriorityColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issueId={issue.id}
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "estimate" ? (
<SpreadsheetEstimateColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issueId={issue.id}
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "assignee" ? (
<SpreadsheetAssigneeColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issueId={issue.id}
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "labels" ? (
<SpreadsheetLabelColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issueId={issue.id}
labels={labels}
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "start_date" ? (
<SpreadsheetStartDateColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issueId={issue.id}
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "due_date" ? (
<SpreadsheetDueDateColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issueId={issue.id}
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "created_on" ? (
<SpreadsheetCreatedOnColumn expandedIssues={expandedIssues} issueId={issue.id} />
) : property === "updated_on" ? (
<SpreadsheetUpdatedOnColumn expandedIssues={expandedIssues} issueId={issue.id} />
) : property === "link" ? (
<SpreadsheetLinkColumn expandedIssues={expandedIssues} issueId={issue.id} />
) : property === "attachment_count" ? (
<SpreadsheetAttachmentColumn expandedIssues={expandedIssues} issueId={issue.id} />
) : property === "sub_issue_count" ? (
<SpreadsheetSubIssueColumn expandedIssues={expandedIssues} issueId={issue.id} />
) : null}
</div>
);
})}
</div>
</div>
);
};

View File

@ -0,0 +1,59 @@
// ui
import { LayersIcon } from "@plane/ui";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
// constants
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
// components
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { SpreadsheetHeaderColumn } from "./columns/header-column";
interface Props {
displayProperties: IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
isEstimateEnabled: boolean;
}
export const SpreadsheetHeader = (props: Props) => {
const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled } = props;
return (
<thead className="sticky top-0 left-0 z-[1] border-b-[0.5px] border-custom-border-100">
<tr>
<th className="sticky left-0 z-[1] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100">
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
<span className="flex h-full w-24 flex-shrink-0 items-center px-4 py-2.5">
<span className="mr-1.5 text-custom-text-400">#</span>ID
</span>
</WithDisplayPropertiesHOC>
<span className="flex h-full w-full flex-grow items-center justify-center px-4 py-2.5">
<LayersIcon className="mr-1.5 h-4 w-4 text-custom-text-400" />
Issue
</span>
</th>
{SPREADSHEET_PROPERTY_LIST.map((property) => {
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
return (
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey={property}
shouldRenderProperty={shouldRenderProperty}
>
<th className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100">
<SpreadsheetHeaderColumn
displayFilters={displayFilters}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
property={property}
/>
</th>
</WithDisplayPropertiesHOC>
);
})}
</tr>
</thead>
);
};

View File

@ -1,20 +1,26 @@
import React, { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues";
import { Spinner, LayersIcon } from "@plane/ui";
import { Spinner } from "@plane/ui";
import { SpreadsheetQuickAddIssueForm } from "components/issues";
// types
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types";
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
import { EIssueActions } from "../types";
import { useProject } from "hooks/store";
import { SpreadsheetHeader } from "./spreadsheet-header";
import { SpreadsheetIssueRow } from "./issue-row";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
type Props = {
displayProperties: IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
issues: TIssue[] | undefined;
labels?: IIssueLabel[] | undefined;
states?: IState[] | undefined;
quickActions: (issue: TIssue, customActionButton: any) => React.ReactNode;
quickActions: (
issue: TIssue,
customActionButton?: React.ReactElement,
portalElement?: HTMLDivElement | null
) => React.ReactNode;
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
openIssuesListModal?: (() => void) | null;
quickAddCallback?: (
@ -35,8 +41,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
displayFilters,
handleDisplayFilterUpdate,
issues,
labels,
states,
quickActions,
handleIssues,
quickAddCallback,
@ -46,16 +50,36 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
disableIssueCreation,
} = props;
// states
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const [isScrolled, setIsScrolled] = useState(false);
const isScrolled = useRef(false);
// refs
const containerRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLTableElement | null>(null);
const portalRef = useRef<HTMLDivElement | null>(null);
const { currentProjectDetails } = useProject();
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
const handleScroll = () => {
if (!containerRef.current) return;
const scrollLeft = containerRef.current.scrollLeft;
setIsScrolled(scrollLeft > 0);
const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
//The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
if (scrollLeft > 0 !== isScrolled.current) {
const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
for (let i = 0; i < firtColumns.length; i++) {
const shadow = i === 0 ? headerShadow : columnShadow;
if (scrollLeft > 0) {
(firtColumns[i] as HTMLElement).style.boxShadow = shadow;
} else {
(firtColumns[i] as HTMLElement).style.boxShadow = "none";
}
}
isScrolled.current = scrollLeft > 0;
}
};
useEffect(() => {
@ -76,105 +100,38 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
);
return (
<div className="relative flex h-full w-full overflow-x-auto whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
<div className="flex h-full w-full flex-col">
<div
ref={containerRef}
className="horizontal-scroll-enable flex divide-x-[0.5px] divide-custom-border-200 overflow-y-auto"
>
{issues && issues.length > 0 && (
<>
<div className="sticky left-0 z-[2] w-[28rem]">
<div
className="relative z-[2] flex h-max w-full flex-col bg-custom-background-100"
style={{
boxShadow: isScrolled ? "8px -9px 12px rgba(0, 0, 0, 0.05)" : "",
}}
>
<div className="sticky top-0 z-[2] flex h-11 w-full items-center border border-l-0 border-custom-border-100 bg-custom-background-90 text-sm font-medium">
{displayProperties.key && (
<span className="flex h-full w-24 flex-shrink-0 items-center px-4 py-2.5">
<span className="mr-1.5 text-custom-text-400">#</span>ID
</span>
)}
<span className="flex h-full w-full flex-grow items-center justify-center px-4 py-2.5">
<LayersIcon className="mr-1.5 h-4 w-4 text-custom-text-400" />
Issue
</span>
</div>
{issues.map((issue, index) =>
issue ? (
<SpreadsheetIssuesColumn
key={`${issue?.id}_${index}`}
issueId={issue.id}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
properties={displayProperties}
quickActions={quickActions}
canEditProperties={canEditProperties}
/>
) : null
)}
</div>
</div>
<SpreadsheetColumnsList
displayFilters={displayFilters}
<div className="relative flex flex-col h-full w-full overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
<div ref={portalRef} className="spreadsheet-menu-portal" />
<div ref={containerRef} className="horizontal-scroll-enable h-full w-full">
<table className="divide-x-[0.5px] divide-custom-border-200 overflow-y-auto">
<SpreadsheetHeader
displayProperties={displayProperties}
displayFilters={displayFilters}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
isEstimateEnabled={isEstimateEnabled}
/>
<tbody>
{issues.map(({ id }) => (
<SpreadsheetIssueRow
key={id}
issueId={id}
displayProperties={displayProperties}
quickActions={quickActions}
canEditProperties={canEditProperties}
expandedIssues={expandedIssues}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
handleUpdateIssue={(issue, data) => handleIssues({ ...issue, ...data }, EIssueActions.UPDATE)}
issues={issues}
labels={labels}
states={states}
nestingLevel={0}
isEstimateEnabled={isEstimateEnabled}
handleIssues={handleIssues}
portalElement={portalRef}
/>
</>
))}
</tbody>
</table>
</div>
<div className="border-t border-custom-border-100">
<div className="z-5 sticky bottom-0 left-0 mb-3">
{enableQuickCreateIssue && !disableIssueCreation && (
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
)}
<div /> {/* empty div to show right most border */}
</div>
<div className="border-t border-custom-border-100">
<div className="z-5 sticky bottom-0 left-0 mb-3">
{enableQuickCreateIssue && !disableIssueCreation && (
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
)}
</div>
{/* {!disableUserActions &&
!isInlineCreateIssueFormOpen &&
(type === "issue" ? (
<button
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] w-full"
onClick={() => setIsInlineCreateIssueFormOpen(true)}
>
<PlusIcon className="h-4 w-4" />
New Issue
</button>
) : (
<CustomMenu
className="sticky left-0 z-10"
customButton={
<button
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
New Issue
</button>
}
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
)}
</CustomMenu>
))} */}
</div>
</div>
</div>

View File

@ -1,8 +1,22 @@
import { TIssueOrderByOptions } from "@plane/types";
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
import { FC } from "react";
import { ISvgIcons } from "@plane/ui/src/icons/type";
import {
SpreadsheetAssigneeColumn,
SpreadsheetAttachmentColumn,
SpreadsheetCreatedOnColumn,
SpreadsheetDueDateColumn,
SpreadsheetEstimateColumn,
SpreadsheetLabelColumn,
SpreadsheetLinkColumn,
SpreadsheetPriorityColumn,
SpreadsheetStartDateColumn,
SpreadsheetStateColumn,
SpreadsheetSubIssueColumn,
SpreadsheetUpdatedOnColumn,
} from "components/issues/issue-layouts/spreadsheet";
export const SPREADSHEET_PROPERTY_DETAILS: {
[key: string]: {
@ -12,6 +26,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: TIssueOrderByOptions;
descendingOrderTitle: string;
icon: FC<ISvgIcons>;
Column: React.FC<{ issue: TIssue; onChange: (issue: TIssue, data: Partial<TIssue>) => void; disabled: boolean }>;
};
} = {
assignee: {
@ -21,6 +36,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-assignees__first_name",
descendingOrderTitle: "Z",
icon: UserGroupIcon,
Column: SpreadsheetAssigneeColumn,
},
created_on: {
title: "Created on",
@ -29,6 +45,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "created_at",
descendingOrderTitle: "Old",
icon: CalendarDays,
Column: SpreadsheetCreatedOnColumn,
},
due_date: {
title: "Due date",
@ -37,6 +54,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "target_date",
descendingOrderTitle: "Old",
icon: CalendarCheck,
Column: SpreadsheetDueDateColumn,
},
estimate: {
title: "Estimate",
@ -45,6 +63,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-estimate_point",
descendingOrderTitle: "High",
icon: Triangle,
Column: SpreadsheetEstimateColumn,
},
labels: {
title: "Labels",
@ -53,6 +72,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-labels__name",
descendingOrderTitle: "Z",
icon: Tag,
Column: SpreadsheetLabelColumn,
},
priority: {
title: "Priority",
@ -61,6 +81,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-priority",
descendingOrderTitle: "Urgent",
icon: Signal,
Column: SpreadsheetPriorityColumn,
},
start_date: {
title: "Start date",
@ -69,6 +90,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "start_date",
descendingOrderTitle: "Old",
icon: CalendarClock,
Column: SpreadsheetStartDateColumn,
},
state: {
title: "State",
@ -77,6 +99,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-state__name",
descendingOrderTitle: "Z",
icon: DoubleCircleIcon,
Column: SpreadsheetStateColumn,
},
updated_on: {
title: "Updated on",
@ -85,6 +108,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "updated_at",
descendingOrderTitle: "Old",
icon: CalendarDays,
Column: SpreadsheetUpdatedOnColumn,
},
link: {
title: "Link",
@ -93,6 +117,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "link_count",
descendingOrderTitle: "Least",
icon: Link2,
Column: SpreadsheetLinkColumn,
},
attachment_count: {
title: "Attachment",
@ -101,6 +126,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "attachment_count",
descendingOrderTitle: "Least",
icon: Paperclip,
Column: SpreadsheetAttachmentColumn,
},
sub_issue_count: {
title: "Sub-issue",
@ -109,5 +135,21 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "sub_issues_count",
descendingOrderTitle: "Least",
icon: LayersIcon,
Column: SpreadsheetSubIssueColumn,
},
};
export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
"state",
"priority",
"assignee",
"labels",
"start_date",
"due_date",
"estimate",
"created_on",
"updated_on",
"link",
"attachment_count",
"sub_issue_count",
];