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", "@headlessui/react": "^1.7.17",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-popper": "^2.3.0" "react-popper": "^2.3.0"
} }
} }

View File

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import ReactDOM from "react-dom";
// react-poppper // react-poppper
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
// hooks // hooks
@ -29,8 +29,10 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
optionsClassName = "", optionsClassName = "",
verticalEllipsis = false, verticalEllipsis = false,
width = "auto", width = "auto",
portalElement,
menuButtonOnClick, menuButtonOnClick,
tabIndex, tabIndex,
closeOnSelect,
} = props; } = props;
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null); const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
@ -51,6 +53,39 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown); 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 ( return (
<Menu <Menu
as="div" as="div"
@ -118,28 +153,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
)} )}
</> </>
)} )}
{isOpen && ( {isOpen && menuItems}
<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>
)}
</> </>
)} )}
</Menu> </Menu>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { FC, useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useIssues, useLabel, useProjectState, useUser } from "hooks/store"; import { useIssues, useUser } from "hooks/store";
// views // views
import { SpreadsheetView } from "./spreadsheet-view"; import { SpreadsheetView } from "./spreadsheet-view";
// types // types
@ -40,10 +40,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const {
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState();
// derived values // derived values
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {}; const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
// user role validation // user role validation
@ -86,27 +82,31 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
[issueFiltersStore, projectId, workspaceSlug, viewId] [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 ( return (
<SpreadsheetView <SpreadsheetView
displayProperties={issueFiltersStore.issueFilters?.displayProperties ?? {}} displayProperties={issueFiltersStore.issueFilters?.displayProperties ?? {}}
displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}} displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate} handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues} issues={issues}
quickActions={(issue, customActionButton) => ( quickActions={renderQuickActions}
<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}
handleIssues={handleIssues} handleIssues={handleIssues}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
quickAddCallback={issueStore.quickAddIssue} quickAddCallback={issueStore.quickAddIssue}

View File

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

View File

@ -1,39 +1,18 @@
import React from "react"; import React from "react";
// hooks import { observer } from "mobx-react-lite";
// types // types
import { useIssueDetail } from "hooks/store"; import { TIssue } from "@plane/types";
type Props = { type Props = {
issueId: string; issue: TIssue;
expandedIssues: string[];
}; };
export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => { export const SpreadsheetAttachmentColumn: React.FC<Props> = observer((props) => {
const { issueId, expandedIssues } = props; const { issue } = 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);
return ( 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">
<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"}
{issueDetail?.attachment_count} {issueDetail?.attachment_count === 1 ? "attachment" : "attachments"} </div>
</div>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className={`h-11`}>
<SpreadsheetAttachmentColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
</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"; import React from "react";
// hooks import { observer } from "mobx-react-lite";
import { useIssueDetail } from "hooks/store";
// helpers // helpers
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
// types // types
import { TIssue } from "@plane/types";
type Props = { type Props = {
issueId: string; issue: TIssue;
expandedIssues: string[];
}; };
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issueId, expandedIssues }) => { export const SpreadsheetCreatedOnColumn: React.FC<Props> = observer((props: Props) => {
const isExpanded = expandedIssues.indexOf(issueId) > -1; const { issue } = props;
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
return ( return (
<> <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">
{issueDetail && ( {renderFormattedDate(issue.created_at)}
<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"> </div>
{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>
))}
</>
); );
}; });

View File

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

View File

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

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 "./assignee-column";
export * from "./attachment-column"; export * from "./attachment-column";
export * from "./columns-list";
export * from "./created-on-column"; export * from "./created-on-column";
export * from "./due-date-column"; export * from "./due-date-column";
export * from "./estimate-column"; export * from "./estimate-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 React from "react";
import { observer } from "mobx-react-lite";
// components // components
import { IssuePropertyLabels } from "../../properties"; import { IssuePropertyLabels } from "../../properties";
// hooks // hooks
import { useIssueDetail, useLabel } from "hooks/store"; import { useLabel } from "hooks/store";
// types // types
import { TIssue, IIssueLabel } from "@plane/types"; import { TIssue } from "@plane/types";
type Props = { type Props = {
issueId: string; issue: TIssue;
onChange: (issue: TIssue, formData: Partial<TIssue>) => void; onChange: (issue: TIssue, data: Partial<TIssue>) => void;
labels: IIssueLabel[] | undefined;
expandedIssues: string[];
disabled: boolean; disabled: boolean;
}; };
export const SpreadsheetLabelColumn: React.FC<Props> = (props) => { export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) => {
const { issueId, onChange, labels, expandedIssues, disabled } = props; const { issue, onChange, disabled } = props;
// hooks // hooks
const { labelMap } = useLabel(); const { labelMap } = useLabel();
const isExpanded = expandedIssues.indexOf(issueId) > -1; const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
// 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]) || [];
return ( return (
<> <IssuePropertyLabels
{issueDetail && ( projectId={issue.project_id ?? null}
<IssuePropertyLabels value={issue.label_ids}
projectId={issueDetail.project_id ?? null} defaultOptions={defaultLabelOptions}
value={issueDetail.label_ids} onChange={(data) => {
defaultOptions={defaultLabelOptions} onChange(issue, { label_ids: data });
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"
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80" hideDropdownArrow
buttonClassName="px-2.5 h-full" maxRender={1}
hideDropdownArrow disabled={disabled}
maxRender={1} placeholderText="Select labels"
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>
))}
</>
); );
}; });

View File

@ -1,39 +1,18 @@
import React from "react"; import React from "react";
// hooks import { observer } from "mobx-react-lite";
import { useIssueDetail } from "hooks/store";
// types // types
import { TIssue } from "@plane/types";
type Props = { type Props = {
issueId: string; issue: TIssue;
expandedIssues: string[];
}; };
export const SpreadsheetLinkColumn: React.FC<Props> = (props) => { export const SpreadsheetLinkColumn: React.FC<Props> = observer((props: Props) => {
const { issueId, expandedIssues } = props; const { issue } = 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);
return ( 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">
<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"}
{issueDetail?.link_count} {issueDetail?.link_count === 1 ? "link" : "links"} </div>
</div>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className={`h-11`}>
<SpreadsheetLinkColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
</div>
))}
</>
); );
}; });

View File

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

View File

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

View File

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

View File

@ -1,37 +1,18 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite";
// hooks // hooks
import { useIssueDetail } from "hooks/store"; import { TIssue } from "@plane/types";
type Props = { type Props = {
issueId: string; issue: TIssue;
expandedIssues: string[];
}; };
export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => { export const SpreadsheetSubIssueColumn: React.FC<Props> = observer((props: Props) => {
const { issueId, expandedIssues } = props; const { issue } = 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);
return ( 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">
<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"}
{issueDetail?.sub_issues_count} {issueDetail?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} </div>
</div>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<div className={`h-11`}>
<SpreadsheetSubIssueColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
</div>
))}
</>
); );
}; });

View File

@ -1,43 +1,19 @@
import React from "react"; import React from "react";
// hooks import { observer } from "mobx-react-lite";
// import useSubIssue from "hooks/use-sub-issue";
// helpers // helpers
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
// types // types
import { useIssueDetail } from "hooks/store"; import { TIssue } from "@plane/types";
type Props = { type Props = {
issueId: string; issue: TIssue;
expandedIssues: string[];
}; };
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => { export const SpreadsheetUpdatedOnColumn: React.FC<Props> = observer((props: Props) => {
const { issueId, expandedIssues } = props; const { issue } = 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);
return ( return (
<> <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">
{issueDetail && ( {renderFormattedDate(issue.updated_at)}
<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"> </div>
{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>
))}
</>
); );
}; });

View File

@ -1,5 +1,4 @@
export * from "./columns"; export * from "./columns";
export * from "./roots"; export * from "./roots";
export * from "./spreadsheet-column";
export * from "./spreadsheet-view"; export * from "./spreadsheet-view";
export * from "./quick-add-issue-form"; 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 React, { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues"; import { Spinner } from "@plane/ui";
import { Spinner, LayersIcon } from "@plane/ui"; import { SpreadsheetQuickAddIssueForm } from "components/issues";
// types // types
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types"; import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
import { EIssueActions } from "../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 = { type Props = {
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions; displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void; handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
issues: TIssue[] | undefined; issues: TIssue[] | undefined;
labels?: IIssueLabel[] | undefined; quickActions: (
states?: IState[] | undefined; issue: TIssue,
quickActions: (issue: TIssue, customActionButton: any) => React.ReactNode; customActionButton?: React.ReactElement,
portalElement?: HTMLDivElement | null
) => React.ReactNode;
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>; handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
quickAddCallback?: ( quickAddCallback?: (
@ -35,8 +41,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
displayFilters, displayFilters,
handleDisplayFilterUpdate, handleDisplayFilterUpdate,
issues, issues,
labels,
states,
quickActions, quickActions,
handleIssues, handleIssues,
quickAddCallback, quickAddCallback,
@ -46,16 +50,36 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
disableIssueCreation, disableIssueCreation,
} = props; } = props;
// states // states
const [expandedIssues, setExpandedIssues] = useState<string[]>([]); const isScrolled = useRef(false);
const [isScrolled, setIsScrolled] = useState(false);
// refs // 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 = () => { const handleScroll = () => {
if (!containerRef.current) return; if (!containerRef.current) return;
const scrollLeft = containerRef.current.scrollLeft; 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(() => { useEffect(() => {
@ -76,105 +100,38 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
); );
return ( 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="relative flex flex-col h-full w-full overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
<div className="flex h-full w-full flex-col"> <div ref={portalRef} className="spreadsheet-menu-portal" />
<div <div ref={containerRef} className="horizontal-scroll-enable h-full w-full">
ref={containerRef} <table className="divide-x-[0.5px] divide-custom-border-200 overflow-y-auto">
className="horizontal-scroll-enable flex divide-x-[0.5px] divide-custom-border-200 overflow-y-auto" <SpreadsheetHeader
> displayProperties={displayProperties}
{issues && issues.length > 0 && ( displayFilters={displayFilters}
<> handleDisplayFilterUpdate={handleDisplayFilterUpdate}
<div className="sticky left-0 z-[2] w-[28rem]"> isEstimateEnabled={isEstimateEnabled}
<div />
className="relative z-[2] flex h-max w-full flex-col bg-custom-background-100" <tbody>
style={{ {issues.map(({ id }) => (
boxShadow: isScrolled ? "8px -9px 12px rgba(0, 0, 0, 0.05)" : "", <SpreadsheetIssueRow
}} key={id}
> issueId={id}
<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}
displayProperties={displayProperties} displayProperties={displayProperties}
quickActions={quickActions}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
expandedIssues={expandedIssues} nestingLevel={0}
handleDisplayFilterUpdate={handleDisplayFilterUpdate} isEstimateEnabled={isEstimateEnabled}
handleUpdateIssue={(issue, data) => handleIssues({ ...issue, ...data }, EIssueActions.UPDATE)} handleIssues={handleIssues}
issues={issues} portalElement={portalRef}
labels={labels}
states={states}
/> />
</> ))}
</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> </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 { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react"; import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
import { FC } from "react"; import { FC } from "react";
import { ISvgIcons } from "@plane/ui/src/icons/type"; 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: { export const SPREADSHEET_PROPERTY_DETAILS: {
[key: string]: { [key: string]: {
@ -12,6 +26,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: TIssueOrderByOptions; descendingOrderKey: TIssueOrderByOptions;
descendingOrderTitle: string; descendingOrderTitle: string;
icon: FC<ISvgIcons>; icon: FC<ISvgIcons>;
Column: React.FC<{ issue: TIssue; onChange: (issue: TIssue, data: Partial<TIssue>) => void; disabled: boolean }>;
}; };
} = { } = {
assignee: { assignee: {
@ -21,6 +36,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-assignees__first_name", descendingOrderKey: "-assignees__first_name",
descendingOrderTitle: "Z", descendingOrderTitle: "Z",
icon: UserGroupIcon, icon: UserGroupIcon,
Column: SpreadsheetAssigneeColumn,
}, },
created_on: { created_on: {
title: "Created on", title: "Created on",
@ -29,6 +45,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "created_at", descendingOrderKey: "created_at",
descendingOrderTitle: "Old", descendingOrderTitle: "Old",
icon: CalendarDays, icon: CalendarDays,
Column: SpreadsheetCreatedOnColumn,
}, },
due_date: { due_date: {
title: "Due date", title: "Due date",
@ -37,6 +54,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "target_date", descendingOrderKey: "target_date",
descendingOrderTitle: "Old", descendingOrderTitle: "Old",
icon: CalendarCheck, icon: CalendarCheck,
Column: SpreadsheetDueDateColumn,
}, },
estimate: { estimate: {
title: "Estimate", title: "Estimate",
@ -45,6 +63,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-estimate_point", descendingOrderKey: "-estimate_point",
descendingOrderTitle: "High", descendingOrderTitle: "High",
icon: Triangle, icon: Triangle,
Column: SpreadsheetEstimateColumn,
}, },
labels: { labels: {
title: "Labels", title: "Labels",
@ -53,6 +72,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-labels__name", descendingOrderKey: "-labels__name",
descendingOrderTitle: "Z", descendingOrderTitle: "Z",
icon: Tag, icon: Tag,
Column: SpreadsheetLabelColumn,
}, },
priority: { priority: {
title: "Priority", title: "Priority",
@ -61,6 +81,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-priority", descendingOrderKey: "-priority",
descendingOrderTitle: "Urgent", descendingOrderTitle: "Urgent",
icon: Signal, icon: Signal,
Column: SpreadsheetPriorityColumn,
}, },
start_date: { start_date: {
title: "Start date", title: "Start date",
@ -69,6 +90,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "start_date", descendingOrderKey: "start_date",
descendingOrderTitle: "Old", descendingOrderTitle: "Old",
icon: CalendarClock, icon: CalendarClock,
Column: SpreadsheetStartDateColumn,
}, },
state: { state: {
title: "State", title: "State",
@ -77,6 +99,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "-state__name", descendingOrderKey: "-state__name",
descendingOrderTitle: "Z", descendingOrderTitle: "Z",
icon: DoubleCircleIcon, icon: DoubleCircleIcon,
Column: SpreadsheetStateColumn,
}, },
updated_on: { updated_on: {
title: "Updated on", title: "Updated on",
@ -85,6 +108,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "updated_at", descendingOrderKey: "updated_at",
descendingOrderTitle: "Old", descendingOrderTitle: "Old",
icon: CalendarDays, icon: CalendarDays,
Column: SpreadsheetUpdatedOnColumn,
}, },
link: { link: {
title: "Link", title: "Link",
@ -93,6 +117,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "link_count", descendingOrderKey: "link_count",
descendingOrderTitle: "Least", descendingOrderTitle: "Least",
icon: Link2, icon: Link2,
Column: SpreadsheetLinkColumn,
}, },
attachment_count: { attachment_count: {
title: "Attachment", title: "Attachment",
@ -101,6 +126,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "attachment_count", descendingOrderKey: "attachment_count",
descendingOrderTitle: "Least", descendingOrderTitle: "Least",
icon: Paperclip, icon: Paperclip,
Column: SpreadsheetAttachmentColumn,
}, },
sub_issue_count: { sub_issue_count: {
title: "Sub-issue", title: "Sub-issue",
@ -109,5 +135,21 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
descendingOrderKey: "sub_issues_count", descendingOrderKey: "sub_issues_count",
descendingOrderTitle: "Least", descendingOrderTitle: "Least",
icon: LayersIcon, 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",
];