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",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-popper": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
|
||||
import ReactDOM from "react-dom";
|
||||
// react-poppper
|
||||
import { usePopper } from "react-popper";
|
||||
// hooks
|
||||
@ -29,8 +29,10 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
optionsClassName = "",
|
||||
verticalEllipsis = false,
|
||||
width = "auto",
|
||||
portalElement,
|
||||
menuButtonOnClick,
|
||||
tabIndex,
|
||||
closeOnSelect,
|
||||
} = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
|
||||
@ -51,6 +53,39 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
let menuItems = (
|
||||
<Menu.Items
|
||||
className="fixed z-10"
|
||||
onClick={() => {
|
||||
if (closeOnSelect) closeDropdown();
|
||||
}}
|
||||
static
|
||||
>
|
||||
<div
|
||||
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
);
|
||||
|
||||
if (portalElement) {
|
||||
menuItems = ReactDOM.createPortal(menuItems, portalElement);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
as="div"
|
||||
@ -118,28 +153,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isOpen && (
|
||||
<Menu.Items className="fixed z-10" static>
|
||||
<div
|
||||
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||
maxHeight === "lg"
|
||||
? "max-h-60"
|
||||
: maxHeight === "md"
|
||||
? "max-h-48"
|
||||
: maxHeight === "rg"
|
||||
? "max-h-36"
|
||||
: maxHeight === "sm"
|
||||
? "max-h-28"
|
||||
: ""
|
||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
)}
|
||||
{isOpen && menuItems}
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
|
@ -24,6 +24,8 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
|
||||
noBorder?: boolean;
|
||||
verticalEllipsis?: boolean;
|
||||
menuButtonOnClick?: (...args: any) => void;
|
||||
closeOnSelect?: boolean;
|
||||
portalElement?: Element | null;
|
||||
}
|
||||
|
||||
export interface ICustomSelectProps extends IDropdownProps {
|
||||
|
@ -4,4 +4,5 @@ export interface IQuickActionProps {
|
||||
handleUpdate?: (data: TIssue) => Promise<void>;
|
||||
handleRemoveFromView?: () => Promise<void>;
|
||||
customActionButton?: React.ReactElement;
|
||||
portalElement?: HTMLDivElement | null;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, customActionButton } = props;
|
||||
const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props;
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
@ -59,11 +59,15 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||
}}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
@ -74,8 +78,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
@ -87,8 +89,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
@ -99,8 +99,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
@ -12,7 +12,7 @@ import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, customActionButton } = props;
|
||||
const { issue, handleDelete, customActionButton, portalElement } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -40,11 +40,15 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={handleDelete}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
@ -55,8 +59,6 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props;
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
@ -59,11 +59,15 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||
}}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
@ -74,8 +78,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit({
|
||||
...issue,
|
||||
cycle: cycleId?.toString() ?? null,
|
||||
@ -90,8 +92,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveFromView && handleRemoveFromView();
|
||||
}}
|
||||
>
|
||||
@ -102,8 +102,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
@ -114,8 +112,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props;
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
@ -59,11 +59,15 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||
}}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
@ -74,8 +78,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
@ -87,8 +89,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveFromView && handleRemoveFromView();
|
||||
}}
|
||||
>
|
||||
@ -99,8 +99,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
@ -16,7 +16,7 @@ import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
|
||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, customActionButton } = props;
|
||||
const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -68,11 +68,15 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||
}}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
@ -85,8 +89,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
<>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
@ -98,8 +100,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
@ -110,8 +110,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
|
||||
import { useGlobalView, useIssues, useUser } from "hooks/store";
|
||||
// components
|
||||
import { GlobalViewsAppliedFiltersRoot } from "components/issues";
|
||||
import { SpreadsheetView } from "components/issues/issue-layouts";
|
||||
@ -37,9 +37,6 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
|
||||
membership: { currentWorkspaceAllProjectsRole },
|
||||
} = useUser();
|
||||
const { fetchAllGlobalViews } = useGlobalView();
|
||||
const {
|
||||
workspace: { workspaceLabels },
|
||||
} = useLabel();
|
||||
// derived values
|
||||
const currentIssueView = type ?? globalViewId;
|
||||
|
||||
@ -134,7 +131,6 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
|
||||
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
||||
/>
|
||||
)}
|
||||
labels={workspaceLabels || undefined}
|
||||
handleIssues={handleIssues}
|
||||
canEditProperties={canEditProperties}
|
||||
viewId={currentIssueView}
|
||||
|
@ -2,7 +2,7 @@ import { FC, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useProjectState, useUser } from "hooks/store";
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
// views
|
||||
import { SpreadsheetView } from "./spreadsheet-view";
|
||||
// types
|
||||
@ -40,10 +40,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const {
|
||||
project: { projectLabels },
|
||||
} = useLabel();
|
||||
const { projectStates } = useProjectState();
|
||||
// derived values
|
||||
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
|
||||
// user role validation
|
||||
@ -86,27 +82,31 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
[issueFiltersStore, projectId, workspaceSlug, viewId]
|
||||
);
|
||||
|
||||
const renderQuickActions = useCallback(
|
||||
(issue: TIssue, customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null) => (
|
||||
<QuickActions
|
||||
customActionButton={customActionButton}
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
||||
handleUpdate={
|
||||
issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
|
||||
}
|
||||
portalElement={portalElement}
|
||||
/>
|
||||
),
|
||||
[handleIssues]
|
||||
);
|
||||
|
||||
return (
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFiltersStore.issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issues={issues}
|
||||
quickActions={(issue, customActionButton) => (
|
||||
<QuickActions
|
||||
customActionButton={customActionButton}
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
||||
handleUpdate={
|
||||
issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
quickActions={renderQuickActions}
|
||||
handleIssues={handleIssues}
|
||||
canEditProperties={canEditProperties}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
|
@ -1,56 +1,34 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { ProjectMemberDropdown } from "components/dropdowns";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
issue: TIssue;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<ProjectMemberDropdown
|
||||
value={issueDetail?.assignee_ids ?? []}
|
||||
onChange={(data) => onChange(issueDetail, { assignee_ids: data })}
|
||||
projectId={issueDetail?.project_id}
|
||||
disabled={disabled}
|
||||
multiple
|
||||
placeholder="Assignees"
|
||||
buttonVariant={issueDetail.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
|
||||
buttonClassName="text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId) => (
|
||||
<SpreadsheetAssigneeColumn
|
||||
key={subIssueId}
|
||||
issueId={subIssueId}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<ProjectMemberDropdown
|
||||
value={issue?.assignee_ids ?? []}
|
||||
onChange={(data) => onChange(issue, { assignee_ids: data })}
|
||||
projectId={issue?.project_id}
|
||||
disabled={disabled}
|
||||
multiple
|
||||
placeholder="Assignees"
|
||||
buttonVariant={
|
||||
issue?.assignee_ids && issue.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text"
|
||||
}
|
||||
buttonClassName="text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,39 +1,18 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
expandedIssues: string[];
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
||||
const { issueId, expandedIssues } = props;
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
|
||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
export const SpreadsheetAttachmentColumn: React.FC<Props> = observer((props) => {
|
||||
const { issue } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issueDetail?.attachment_count} {issueDetail?.attachment_count === 1 ? "attachment" : "attachments"}
|
||||
</div>
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId: string) => (
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetAttachmentColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
expandedIssues: string[];
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issueId, expandedIssues }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
|
||||
export const SpreadsheetCreatedOnColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue } = props;
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{renderFormattedDate(issueDetail.created_at)}
|
||||
</div>
|
||||
)}
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId: string) => (
|
||||
<div className="h-11">
|
||||
<SpreadsheetCreatedOnColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{renderFormattedDate(issue.created_at)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { DateDropdown } from "components/dropdowns";
|
||||
// helpers
|
||||
@ -9,49 +8,25 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
issue: TIssue;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<DateDropdown
|
||||
value={issueDetail.target_date}
|
||||
onChange={(data) => onChange(issueDetail, { target_date: data ? renderFormattedPayloadDate(data) : null })}
|
||||
disabled={disabled}
|
||||
placeholder="Due date"
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId) => (
|
||||
<SpreadsheetDueDateColumn
|
||||
key={subIssueId}
|
||||
issueId={subIssueId}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<DateDropdown
|
||||
value={issue.target_date}
|
||||
onChange={(data) => onChange(issue, { target_date: data ? renderFormattedPayloadDate(data) : null })}
|
||||
disabled={disabled}
|
||||
placeholder="Due date"
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,56 +1,29 @@
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { EstimateDropdown } from "components/dropdowns";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
issue: TIssue;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
||||
const { issueId, onChange, expandedIssues, disabled } = props;
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<EstimateDropdown
|
||||
value={issueDetail.estimate_point}
|
||||
onChange={(data) => onChange(issueDetail, { estimate_point: data })}
|
||||
projectId={issueDetail.project_id}
|
||||
disabled={disabled}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId) => (
|
||||
<SpreadsheetEstimateColumn
|
||||
key={subIssueId}
|
||||
issueId={subIssueId}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<EstimateDropdown
|
||||
value={issue.estimate_point}
|
||||
onChange={(data) => onChange(issue, { estimate_point: data })}
|
||||
projectId={issue.project_id}
|
||||
disabled={disabled}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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 "./attachment-column";
|
||||
export * from "./columns-list";
|
||||
export * from "./created-on-column";
|
||||
export * from "./due-date-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 { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssuePropertyLabels } from "../../properties";
|
||||
// hooks
|
||||
import { useIssueDetail, useLabel } from "hooks/store";
|
||||
import { useLabel } from "hooks/store";
|
||||
// types
|
||||
import { TIssue, IIssueLabel } from "@plane/types";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
|
||||
labels: IIssueLabel[] | undefined;
|
||||
expandedIssues: string[];
|
||||
issue: TIssue;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
const { issueId, onChange, labels, expandedIssues, disabled } = props;
|
||||
export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
// hooks
|
||||
const { labelMap } = useLabel();
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
|
||||
const defaultLabelOptions = issueDetail?.label_ids?.map((id) => labelMap[id]) || [];
|
||||
const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<IssuePropertyLabels
|
||||
projectId={issueDetail.project_id ?? null}
|
||||
value={issueDetail.label_ids}
|
||||
defaultOptions={defaultLabelOptions}
|
||||
onChange={(data) => {
|
||||
onChange(issueDetail, { label_ids: data });
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="px-2.5 h-full"
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
disabled={disabled}
|
||||
placeholderText="Select labels"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId: string) => (
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetLabelColumn
|
||||
key={subIssueId}
|
||||
issueId={subIssueId}
|
||||
onChange={onChange}
|
||||
labels={labels}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<IssuePropertyLabels
|
||||
projectId={issue.project_id ?? null}
|
||||
value={issue.label_ids}
|
||||
defaultOptions={defaultLabelOptions}
|
||||
onChange={(data) => {
|
||||
onChange(issue, { label_ids: data });
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="px-2.5 h-full"
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
disabled={disabled}
|
||||
placeholderText="Select labels"
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,39 +1,18 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
expandedIssues: string[];
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
||||
const { issueId, expandedIssues } = props;
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
export const SpreadsheetLinkColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issueDetail?.link_count} {issueDetail?.link_count === 1 ? "link" : "links"}
|
||||
</div>
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId: string) => (
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetLinkColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issue?.link_count} {issue?.link_count === 1 ? "link" : "links"}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,54 +1,29 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { PriorityDropdown } from "components/dropdowns";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
issue: TIssue;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetPriorityColumn: React.FC<Props> = (props) => {
|
||||
const { issueId, onChange, expandedIssues, disabled } = props;
|
||||
// store hooks
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
// derived values
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<PriorityDropdown
|
||||
value={issueDetail.priority}
|
||||
onChange={(data) => onChange(issueDetail, { priority: data })}
|
||||
disabled={disabled}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId: string) => (
|
||||
<SpreadsheetPriorityColumn
|
||||
key={subIssueId}
|
||||
issueId={subIssueId}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<PriorityDropdown
|
||||
value={issue.priority}
|
||||
onChange={(data) => onChange(issue, { priority: data })}
|
||||
disabled={disabled}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { DateDropdown } from "components/dropdowns";
|
||||
// helpers
|
||||
@ -9,50 +8,25 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
issue: TIssue;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<DateDropdown
|
||||
value={issueDetail.start_date}
|
||||
onChange={(data) => onChange(issueDetail, { start_date: data ? renderFormattedPayloadDate(data) : null })}
|
||||
disabled={disabled}
|
||||
placeholder="Start date"
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId) => (
|
||||
<SpreadsheetStartDateColumn
|
||||
key={subIssueId}
|
||||
issueId={subIssueId}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<DateDropdown
|
||||
value={issue.start_date}
|
||||
onChange={(data) => onChange(issue, { start_date: data ? renderFormattedPayloadDate(data) : null })}
|
||||
disabled={disabled}
|
||||
placeholder="Start date"
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,59 +1,30 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { StateDropdown } from "components/dropdowns";
|
||||
// types
|
||||
import { TIssue, IState } from "@plane/types";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
issue: TIssue;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||
states: IState[] | undefined;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
const { issueId, onChange, states, expandedIssues, disabled } = props;
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<StateDropdown
|
||||
projectId={issueDetail.project_id}
|
||||
value={issueDetail.state_id}
|
||||
onChange={(data) => onChange(issueDetail, { state_id: data })}
|
||||
disabled={disabled}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId) => (
|
||||
<SpreadsheetStateColumn
|
||||
key={subIssueId}
|
||||
issueId={subIssueId}
|
||||
onChange={onChange}
|
||||
states={states}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<StateDropdown
|
||||
projectId={issue.project_id}
|
||||
value={issue.state_id}
|
||||
onChange={(data) => onChange(issue, { state_id: data })}
|
||||
disabled={disabled}
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,37 +1,18 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
expandedIssues: string[];
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
|
||||
const { issueId, expandedIssues } = props;
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
export const SpreadsheetSubIssueColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issueDetail?.sub_issues_count} {issueDetail?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId: string) => (
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetSubIssueColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,43 +1,19 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
// import useSubIssue from "hooks/use-sub-issue";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
expandedIssues: string[];
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
||||
const { issueId, expandedIssues } = props;
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
||||
|
||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
|
||||
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue } = props;
|
||||
return (
|
||||
<>
|
||||
{issueDetail && (
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{renderFormattedDate(issueDetail.updated_at)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssueId: string) => (
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetUpdatedOnColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{renderFormattedDate(issue.updated_at)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,5 +1,4 @@
|
||||
export * from "./columns";
|
||||
export * from "./roots";
|
||||
export * from "./spreadsheet-column";
|
||||
export * from "./spreadsheet-view";
|
||||
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 { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues";
|
||||
import { Spinner, LayersIcon } from "@plane/ui";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { SpreadsheetQuickAddIssueForm } from "components/issues";
|
||||
// types
|
||||
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types";
|
||||
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { useProject } from "hooks/store";
|
||||
import { SpreadsheetHeader } from "./spreadsheet-header";
|
||||
import { SpreadsheetIssueRow } from "./issue-row";
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
|
||||
type Props = {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayFilters: IIssueDisplayFilterOptions;
|
||||
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||
issues: TIssue[] | undefined;
|
||||
labels?: IIssueLabel[] | undefined;
|
||||
states?: IState[] | undefined;
|
||||
quickActions: (issue: TIssue, customActionButton: any) => React.ReactNode;
|
||||
quickActions: (
|
||||
issue: TIssue,
|
||||
customActionButton?: React.ReactElement,
|
||||
portalElement?: HTMLDivElement | null
|
||||
) => React.ReactNode;
|
||||
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
quickAddCallback?: (
|
||||
@ -35,8 +41,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
displayFilters,
|
||||
handleDisplayFilterUpdate,
|
||||
issues,
|
||||
labels,
|
||||
states,
|
||||
quickActions,
|
||||
handleIssues,
|
||||
quickAddCallback,
|
||||
@ -46,16 +50,36 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
disableIssueCreation,
|
||||
} = props;
|
||||
// states
|
||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const isScrolled = useRef(false);
|
||||
// refs
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const containerRef = useRef<HTMLTableElement | null>(null);
|
||||
const portalRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { currentProjectDetails } = useProject();
|
||||
|
||||
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const scrollLeft = containerRef.current.scrollLeft;
|
||||
setIsScrolled(scrollLeft > 0);
|
||||
|
||||
const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
|
||||
const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
|
||||
|
||||
//The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
|
||||
if (scrollLeft > 0 !== isScrolled.current) {
|
||||
const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
|
||||
|
||||
for (let i = 0; i < firtColumns.length; i++) {
|
||||
const shadow = i === 0 ? headerShadow : columnShadow;
|
||||
if (scrollLeft > 0) {
|
||||
(firtColumns[i] as HTMLElement).style.boxShadow = shadow;
|
||||
} else {
|
||||
(firtColumns[i] as HTMLElement).style.boxShadow = "none";
|
||||
}
|
||||
}
|
||||
isScrolled.current = scrollLeft > 0;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -76,105 +100,38 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full overflow-x-auto whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="horizontal-scroll-enable flex divide-x-[0.5px] divide-custom-border-200 overflow-y-auto"
|
||||
>
|
||||
{issues && issues.length > 0 && (
|
||||
<>
|
||||
<div className="sticky left-0 z-[2] w-[28rem]">
|
||||
<div
|
||||
className="relative z-[2] flex h-max w-full flex-col bg-custom-background-100"
|
||||
style={{
|
||||
boxShadow: isScrolled ? "8px -9px 12px rgba(0, 0, 0, 0.05)" : "",
|
||||
}}
|
||||
>
|
||||
<div className="sticky top-0 z-[2] flex h-11 w-full items-center border border-l-0 border-custom-border-100 bg-custom-background-90 text-sm font-medium">
|
||||
{displayProperties.key && (
|
||||
<span className="flex h-full w-24 flex-shrink-0 items-center px-4 py-2.5">
|
||||
<span className="mr-1.5 text-custom-text-400">#</span>ID
|
||||
</span>
|
||||
)}
|
||||
<span className="flex h-full w-full flex-grow items-center justify-center px-4 py-2.5">
|
||||
<LayersIcon className="mr-1.5 h-4 w-4 text-custom-text-400" />
|
||||
Issue
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{issues.map((issue, index) =>
|
||||
issue ? (
|
||||
<SpreadsheetIssuesColumn
|
||||
key={`${issue?.id}_${index}`}
|
||||
issueId={issue.id}
|
||||
expandedIssues={expandedIssues}
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
properties={displayProperties}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SpreadsheetColumnsList
|
||||
displayFilters={displayFilters}
|
||||
<div className="relative flex flex-col h-full w-full overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
|
||||
<div ref={portalRef} className="spreadsheet-menu-portal" />
|
||||
<div ref={containerRef} className="horizontal-scroll-enable h-full w-full">
|
||||
<table className="divide-x-[0.5px] divide-custom-border-200 overflow-y-auto">
|
||||
<SpreadsheetHeader
|
||||
displayProperties={displayProperties}
|
||||
displayFilters={displayFilters}
|
||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
/>
|
||||
<tbody>
|
||||
{issues.map(({ id }) => (
|
||||
<SpreadsheetIssueRow
|
||||
key={id}
|
||||
issueId={id}
|
||||
displayProperties={displayProperties}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
expandedIssues={expandedIssues}
|
||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||
handleUpdateIssue={(issue, data) => handleIssues({ ...issue, ...data }, EIssueActions.UPDATE)}
|
||||
issues={issues}
|
||||
labels={labels}
|
||||
states={states}
|
||||
nestingLevel={0}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
handleIssues={handleIssues}
|
||||
portalElement={portalRef}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="border-t border-custom-border-100">
|
||||
<div className="z-5 sticky bottom-0 left-0 mb-3">
|
||||
{enableQuickCreateIssue && !disableIssueCreation && (
|
||||
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||
)}
|
||||
<div /> {/* empty div to show right most border */}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-custom-border-100">
|
||||
<div className="z-5 sticky bottom-0 left-0 mb-3">
|
||||
{enableQuickCreateIssue && !disableIssueCreation && (
|
||||
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* {!disableUserActions &&
|
||||
!isInlineCreateIssueFormOpen &&
|
||||
(type === "issue" ? (
|
||||
<button
|
||||
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] w-full"
|
||||
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
New Issue
|
||||
</button>
|
||||
) : (
|
||||
<CustomMenu
|
||||
className="sticky left-0 z-10"
|
||||
customButton={
|
||||
<button
|
||||
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full"
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
New Issue
|
||||
</button>
|
||||
}
|
||||
optionsClassName="left-5 !w-36"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
))} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,22 @@
|
||||
import { TIssueOrderByOptions } from "@plane/types";
|
||||
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
|
||||
import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
||||
import {
|
||||
SpreadsheetAssigneeColumn,
|
||||
SpreadsheetAttachmentColumn,
|
||||
SpreadsheetCreatedOnColumn,
|
||||
SpreadsheetDueDateColumn,
|
||||
SpreadsheetEstimateColumn,
|
||||
SpreadsheetLabelColumn,
|
||||
SpreadsheetLinkColumn,
|
||||
SpreadsheetPriorityColumn,
|
||||
SpreadsheetStartDateColumn,
|
||||
SpreadsheetStateColumn,
|
||||
SpreadsheetSubIssueColumn,
|
||||
SpreadsheetUpdatedOnColumn,
|
||||
} from "components/issues/issue-layouts/spreadsheet";
|
||||
|
||||
export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
[key: string]: {
|
||||
@ -12,6 +26,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: TIssueOrderByOptions;
|
||||
descendingOrderTitle: string;
|
||||
icon: FC<ISvgIcons>;
|
||||
Column: React.FC<{ issue: TIssue; onChange: (issue: TIssue, data: Partial<TIssue>) => void; disabled: boolean }>;
|
||||
};
|
||||
} = {
|
||||
assignee: {
|
||||
@ -21,6 +36,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "-assignees__first_name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: UserGroupIcon,
|
||||
Column: SpreadsheetAssigneeColumn,
|
||||
},
|
||||
created_on: {
|
||||
title: "Created on",
|
||||
@ -29,6 +45,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "created_at",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarDays,
|
||||
Column: SpreadsheetCreatedOnColumn,
|
||||
},
|
||||
due_date: {
|
||||
title: "Due date",
|
||||
@ -37,6 +54,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "target_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarCheck,
|
||||
Column: SpreadsheetDueDateColumn,
|
||||
},
|
||||
estimate: {
|
||||
title: "Estimate",
|
||||
@ -45,6 +63,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "-estimate_point",
|
||||
descendingOrderTitle: "High",
|
||||
icon: Triangle,
|
||||
Column: SpreadsheetEstimateColumn,
|
||||
},
|
||||
labels: {
|
||||
title: "Labels",
|
||||
@ -53,6 +72,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "-labels__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: Tag,
|
||||
Column: SpreadsheetLabelColumn,
|
||||
},
|
||||
priority: {
|
||||
title: "Priority",
|
||||
@ -61,6 +81,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "-priority",
|
||||
descendingOrderTitle: "Urgent",
|
||||
icon: Signal,
|
||||
Column: SpreadsheetPriorityColumn,
|
||||
},
|
||||
start_date: {
|
||||
title: "Start date",
|
||||
@ -69,6 +90,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "start_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarClock,
|
||||
Column: SpreadsheetStartDateColumn,
|
||||
},
|
||||
state: {
|
||||
title: "State",
|
||||
@ -77,6 +99,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "-state__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: DoubleCircleIcon,
|
||||
Column: SpreadsheetStateColumn,
|
||||
},
|
||||
updated_on: {
|
||||
title: "Updated on",
|
||||
@ -85,6 +108,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "updated_at",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarDays,
|
||||
Column: SpreadsheetUpdatedOnColumn,
|
||||
},
|
||||
link: {
|
||||
title: "Link",
|
||||
@ -93,6 +117,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "link_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: Link2,
|
||||
Column: SpreadsheetLinkColumn,
|
||||
},
|
||||
attachment_count: {
|
||||
title: "Attachment",
|
||||
@ -101,6 +126,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "attachment_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: Paperclip,
|
||||
Column: SpreadsheetAttachmentColumn,
|
||||
},
|
||||
sub_issue_count: {
|
||||
title: "Sub-issue",
|
||||
@ -109,5 +135,21 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
descendingOrderKey: "sub_issues_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: LayersIcon,
|
||||
Column: SpreadsheetSubIssueColumn,
|
||||
},
|
||||
};
|
||||
|
||||
export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
|
||||
"state",
|
||||
"priority",
|
||||
"assignee",
|
||||
"labels",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"estimate",
|
||||
"created_on",
|
||||
"updated_on",
|
||||
"link",
|
||||
"attachment_count",
|
||||
"sub_issue_count",
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user