forked from github/plane
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:
parent
4611ec0b83
commit
df97b35a99
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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";
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
export * from "./spreadsheet-issue-column";
|
|
||||||
export * from "./issue-column";
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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";
|
||||||
|
186
web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
Normal file
186
web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
@ -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",
|
||||||
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user