dev: implemented the new spreadsheet layout using MobX (#2463)

* refactor: spreadsheet layout components

* refactor: spreadsheet properties

* refactor: folder structure

* chore: issue property update

* chore: spreadsheet layout in the global views

* style: quick actions menu

* fix: build errors
This commit is contained in:
Aaryan Khandelwal 2023-10-18 12:32:02 +05:30 committed by GitHub
parent e9cc578cca
commit 3197dd484c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 2147 additions and 3197 deletions

View file

@ -1,3 +1,2 @@
export * from "./spreadsheet-view";
export * from "./issues-view";
export * from "./inline-issue-create-wrapper";

View file

@ -1,65 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
// components
import { MembersSelect } from "components/project";
// services
import { TrackEventService } from "services/track_event.service";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
onChange: (formData: Partial<IIssue>) => void;
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
const trackEventService = new TrackEventService();
export const AssigneeColumn: React.FC<Props> = ({ issue, projectId, onChange, properties, user, isNotAllowed }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
onChange({ assignees_list: data });
trackEventService.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user as IUser
);
};
return (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.assignee && (
<MembersSelect
value={issue.assignees}
projectId={projectId}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
</span>
</div>
);
};

View file

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

View file

@ -1,62 +0,0 @@
import React from "react";
// components
import { AssigneeColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
handleUpdateIssue: (issueId: string, data: Partial<IIssue>) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({
issue,
projectId,
handleUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<AssigneeColumn
issue={issue}
projectId={projectId}
properties={properties}
onChange={(data) => handleUpdateIssue(issue.id, data)}
user={user}
isNotAllowed={isNotAllowed}
/>
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetAssigneeColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
handleUpdateIssue={handleUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

@ -1,23 +0,0 @@
import React from "react";
// types
import { IIssue, Properties } from "types";
// helper
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
type Props = {
issue: IIssue;
properties: Properties;
};
export const CreatedOnColumn: React.FC<Props> = ({ issue, properties }) => (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.created_on && (
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{renderLongDetailDateFormat(issue.created_at)}
</div>
)}
</span>
</div>
);

View file

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

View file

@ -1,52 +0,0 @@
import React from "react";
// components
import { CreatedOnColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
handleUpdateIssue: (formData: Partial<IIssue>) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({
issue,
handleUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<CreatedOnColumn issue={issue} properties={properties} />
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetCreatedOnColumn
key={subIssue.id}
issue={subIssue}
handleUpdateIssue={handleUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

@ -1,33 +0,0 @@
import { FC } from "react";
// components
import { ViewDueDateSelect } from "components/issues";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const DueDateColumn: FC<Props> = (props) => {
const { issue, partialUpdateIssue, properties, user, isNotAllowed } = props;
return (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.due_date && user && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
)}
</span>
</div>
);
};

View file

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

View file

@ -1,62 +0,0 @@
import React from "react";
// components
import { DueDateColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetDueDateColumn: React.FC<Props> = ({
issue,
projectId,
partialUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<DueDateColumn
issue={issue}
projectId={projectId}
properties={properties}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetDueDateColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
partialUpdateIssue={partialUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

@ -1,34 +0,0 @@
import { FC } from "react";
// components
import { ViewEstimateSelect } from "components/issues";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const EstimateColumn: FC<Props> = (props) => {
const { issue, partialUpdateIssue, properties, user, isNotAllowed } = props;
return (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.estimate && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
user={user}
isNotAllowed={isNotAllowed}
/>
)}
</span>
</div>
);
};

View file

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

View file

@ -1,62 +0,0 @@
import React from "react";
// components
import { EstimateColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetEstimateColumn: React.FC<Props> = ({
issue,
projectId,
partialUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<EstimateColumn
issue={issue}
projectId={projectId}
properties={properties}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetEstimateColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
partialUpdateIssue={partialUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

@ -1,13 +0,0 @@
export * from "./assignee-column";
export * from "./created-on-column";
export * from "./due-date-column";
export * from "./estimate-column";
export * from "./issue-column";
export * from "./label-column";
export * from "./priority-column";
export * from "./start-date-column";
export * from "./state-column";
export * from "./updated-on-column";
export * from "./spreadsheet-view";
export * from "./issue-column/issue-column";
export * from "./issue-column/spreadsheet-issue-column";

View file

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

View file

@ -1,167 +0,0 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
// components
import { Popover2 } from "@blueprintjs/popover2";
// icons
import { MoreHorizontal, LinkIcon, Pencil, Trash2, ChevronRight } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
// types
import { IIssue, Properties } from "types";
// helper
import { copyTextToClipboard } from "helpers/string.helper";
type Props = {
issue: IIssue;
projectId: string;
expanded: boolean;
handleToggleExpand: (issueId: string) => void;
properties: Properties;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
disableUserActions: boolean;
nestingLevel: number;
};
export const IssueColumn: React.FC<Props> = ({
issue,
projectId,
expanded,
handleToggleExpand,
properties,
handleEditIssue,
handleDeleteIssue,
setCurrentProjectId,
disableUserActions,
nestingLevel,
}) => {
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const openPeekOverview = () => {
const { query } = router;
setCurrentProjectId(issue.project_detail.id);
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const paddingLeft = `${nestingLevel * 54}px`;
return (
<div className="group flex items-center w-[28rem] text-sm h-11 sticky top-0 bg-custom-background-100 truncate border-b border-r border-custom-border-100">
{properties.key && (
<div
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center min-w-[96px]"
style={issue.parent && nestingLevel !== 0 ? { paddingLeft } : {}}
>
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
<span className="flex items-center justify-center font-medium opacity-100 group-hover:opacity-0 ">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
{!disableUserActions && (
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
<Popover2
isOpen={isOpen}
canEscapeKeyClose
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
content={
<div
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-100 bg-custom-background-90`}
>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleEditIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<Pencil className="h-4 w-4" />
<span>Edit issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleDeleteIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<span>Delete issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</button>
</div>
}
placement="bottom-start"
>
<MoreHorizontal className="h-5 w-5 text-custom-text-200" />
</Popover2>
</div>
)}
</div>
{issue.sub_issues_count > 0 && (
<div className="h-6 w-6 flex justify-center items-center">
<button
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
onClick={() => handleToggleExpand(issue.id)}
>
<ChevronRight className={`h-3.5 w-3.5 ${expanded ? "rotate-90" : ""}`} />
</button>
</div>
)}
</div>
)}
<span className="flex items-center px-4 py-2.5 h-full truncate flex-grow">
<button
type="button"
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
onClick={openPeekOverview}
>
{issue.name}
</button>
</span>
</div>
);
};

View file

@ -1,85 +0,0 @@
import React from "react";
// components
import { IssueColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
expandedIssues: string[];
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
properties: Properties;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
disableUserActions: boolean;
nestingLevel?: number;
};
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
issue,
projectId,
expandedIssues,
setExpandedIssues,
properties,
handleIssueAction,
setCurrentProjectId,
disableUserActions,
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(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<IssueColumn
issue={issue}
projectId={projectId}
expanded={isExpanded}
handleToggleExpand={handleToggleExpand}
properties={properties}
handleEditIssue={() => handleIssueAction(issue, "edit")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
setCurrentProjectId={setCurrentProjectId}
disableUserActions={disableUserActions}
nestingLevel={nestingLevel}
/>
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetIssuesColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
properties={properties}
handleIssueAction={handleIssueAction}
setCurrentProjectId={setCurrentProjectId}
disableUserActions={disableUserActions}
nestingLevel={nestingLevel + 1}
/>
))}
</div>
);
};

View file

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

View file

@ -1,47 +0,0 @@
import React from "react";
// components
import { LabelSelect } from "components/project";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const LabelColumn: React.FC<Props> = ({
issue,
projectId,
partialUpdateIssue,
properties,
user,
isNotAllowed,
}) => {
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
return (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.labels && (
<LabelSelect
value={issue.labels}
projectId={projectId}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
maxRender={1}
user={user}
disabled={isNotAllowed}
/>
)}
</span>
</div>
);
};

View file

@ -1,62 +0,0 @@
import React from "react";
// components
import { LabelColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetLabelColumn: React.FC<Props> = ({
issue,
projectId,
partialUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<LabelColumn
issue={issue}
projectId={projectId}
properties={properties}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetLabelColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
partialUpdateIssue={partialUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

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

View file

@ -1,58 +0,0 @@
import { FC } from "react";
import { useRouter } from "next/router";
// components
import { PrioritySelect } from "components/project";
// services
import { TrackEventService } from "services/track_event.service";
// types
import { IUser, IIssue, Properties, TIssuePriorities } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
const trackEventService = new TrackEventService();
export const PriorityColumn: FC<Props> = (props) => {
const { issue, partialUpdateIssue, properties, user, isNotAllowed } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventService.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user as IUser
);
};
return (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.priority && (
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
</span>
</div>
);
};

View file

@ -1,62 +0,0 @@
import React from "react";
// components
import { PriorityColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetPriorityColumn: React.FC<Props> = ({
issue,
projectId,
partialUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<PriorityColumn
issue={issue}
projectId={projectId}
properties={properties}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetPriorityColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
partialUpdateIssue={partialUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

@ -1,448 +0,0 @@
import { FC, useCallback, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { Popover2 } from "@blueprintjs/popover2";
// components
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
// icons
import { MoreHorizontal, LinkIcon, Pencil, Trash2, ChevronRight } from "lucide-react";
// services
import { IssueService } from "services/issue";
import { TrackEventService } from "services/track_event.service";
// constant
import {
CYCLE_DETAILS,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES,
VIEW_ISSUES,
} from "constants/fetch-keys";
// types
import { IUser, IIssue, IState, ISubIssueResponse, Properties, TIssuePriorities, UserAuth } from "types";
// helper
import { copyTextToClipboard } from "helpers/string.helper";
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
// hooks
import useToast from "hooks/use-toast";
type Props = {
issue: IIssue;
projectId: string;
index: number;
expanded: boolean;
handleToggleExpand: (issueId: string) => void;
properties: Properties;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
gridTemplateColumns: string;
disableUserActions: boolean;
user: IUser | undefined;
userAuth: UserAuth;
nestingLevel: number;
};
const issueService = new IssueService();
const trackEventService = new TrackEventService();
export const SingleSpreadsheetIssue: FC<Props> = (props) => {
const {
issue,
projectId,
index,
expanded,
handleToggleExpand,
properties,
handleEditIssue,
handleDeleteIssue,
gridTemplateColumns,
disableUserActions,
user,
userAuth,
nestingLevel,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
const params = {};
const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return;
const fetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: viewId
? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId, params);
if (issue.parent)
mutate<ISubIssueResponse>(
SUB_ISSUES(issue.parent.toString()),
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
sub_issues: (prevData.sub_issues ?? []).map((i) => {
if (i.id === issue.id) {
return {
...i,
...formData,
};
}
return i;
}),
};
},
false
);
else
mutate<IIssue[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issue.id) {
return {
...p,
...formData,
};
}
return p;
}),
false
);
issueService
.patchIssue(workspaceSlug as string, projectId, issue.id as string, formData, user)
.then(() => {
if (issue.parent) {
mutate(SUB_ISSUES(issue.parent as string));
} else {
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
}
})
.catch((error) => {
console.log(error);
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[workspaceSlug, projectId, cycleId, moduleId, user]
);
const openPeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
partialUpdateIssue(
{
state: data,
state_detail: newState,
},
issue
);
trackEventService.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user as IUser
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventService.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user as IUser
);
}
};
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventService.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user as IUser
);
};
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue);
trackEventService.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user as IUser
);
};
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
const paddingLeft = `${nestingLevel * 68}px`;
const tooltipPosition = index === 0 ? "bottom" : "top";
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<>
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
<div className="flex gap-1.5 items-center px-4 sticky z-[1] left-0 text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full">
<div className="flex gap-1.5 items-center" style={issue.parent ? { paddingLeft } : {}}>
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100 w-14">
{properties.key && (
<span className="flex items-center justify-center opacity-100 group-hover:opacity-0">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
)}
{!isNotAllowed && !disableUserActions && (
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
<Popover2
isOpen={isOpen}
canEscapeKeyClose
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
content={
<div
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleEditIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<Pencil className="h-4 w-4" />
<span>Edit issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleDeleteIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<span>Delete issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</button>
</div>
}
placement="bottom-start"
>
<MoreHorizontal className="h-5 w-5 text-custom-text-200" />
</Popover2>
</div>
)}
</div>
{issue.sub_issues_count > 0 && (
<div className="h-6 w-6 flex justify-center items-center">
<button
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
onClick={() => handleToggleExpand(issue.id)}
>
<ChevronRight className={`h-3.5 w-3.5 ${expanded ? "rotate-90" : ""}`} />
</button>
</div>
)}
</div>
<button
type="button"
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
onClick={openPeekOverview}
>
{issue.name}
</button>
</div>
{properties.state && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<StateSelect
value={issue.state_detail}
projectId={projectId}
onChange={handleStateChange}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
/>
</div>
)}
{properties.priority && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
/>
</div>
)}
{properties.assignee && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<MembersSelect
value={issue.assignees}
projectId={projectId}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
/>
</div>
)}
{properties.labels && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<LabelSelect
value={issue.labels}
projectId={projectId}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
maxRender={1}
user={user}
disabled={isNotAllowed}
/>
</div>
)}
{properties.start_date && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.due_date && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{user && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
)}
</div>
)}
{properties.estimate && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.created_on && (
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{renderLongDetailDateFormat(issue.created_at)}
</div>
)}
{properties.updated_on && (
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{renderLongDetailDateFormat(issue.updated_at)}
</div>
)}
</div>
</>
);
};

View file

@ -1,402 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components
import {
// ListInlineCreateIssueForm,
SpreadsheetAssigneeColumn,
SpreadsheetCreatedOnColumn,
SpreadsheetDueDateColumn,
SpreadsheetEstimateColumn,
SpreadsheetIssuesColumn,
SpreadsheetLabelColumn,
SpreadsheetPriorityColumn,
SpreadsheetStartDateColumn,
SpreadsheetStateColumn,
SpreadsheetUpdatedOnColumn,
} from "components/core";
import { CustomMenu } from "components/ui";
import { IssuePeekOverview } from "components/issues";
import { Spinner } from "@plane/ui";
// types
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "types";
// icon
import {
ArrowDownWideNarrow,
ArrowUpNarrowWide,
CheckIcon,
ChevronDownIcon,
Eraser,
ListFilter,
MoveRight,
PlusIcon,
} from "lucide-react";
type Props = {
displayProperties: IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
issues: IIssue[] | undefined;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleUpdateIssue: (issueId: string, data: Partial<IIssue>) => void;
openIssuesListModal?: (() => void) | null;
disableUserActions: boolean;
};
export const SpreadsheetView: React.FC<Props> = observer((props) => {
const {
displayProperties,
displayFilters,
handleDisplayFilterUpdate,
issues,
handleIssueAction,
handleUpdateIssue,
openIssuesListModal,
disableUserActions,
} = props;
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();
const { workspaceSlug, cycleId, moduleId } = router.query;
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
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 renderColumn = (
header: string,
propertyName: string,
Component: React.ComponentType<any>,
ascendingOrder: TIssueOrderByOptions,
descendingOrder: TIssueOrderByOptions
) => (
<div className="relative flex flex-col h-max w-full bg-custom-background-100">
<div className="flex items-center min-w-[9rem] px-4 py-2.5 text-sm font-medium z-[1] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-100">
<CustomMenu
customButtonClassName="!w-full"
className="!w-full"
customButton={
<div
className={`relative group flex items-center justify-between gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
activeSortingProperty === propertyName ? "bg-custom-background-80" : ""
}`}
>
{activeSortingProperty === propertyName && (
<div className="absolute top-1 right-1.5 bg-custom-primary rounded-full flex items-center justify-center h-3.5 w-3.5">
<ListFilter className="h-3 w-3 text-white" />
</div>
)}
{header}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
width="xl"
>
<CustomMenu.MenuItem
onClick={() => {
handleOrderBy(ascendingOrder, propertyName);
}}
>
<div
className={`group flex gap-1.5 px-1 items-center justify-between ${
selectedMenuItem === `${ascendingOrder}_${propertyName}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex gap-2 items-center">
{propertyName === "assignee" || propertyName === "labels" ? (
<>
<ArrowDownWideNarrow className="h-4 w-4 stroke-[1.5]" />
<span>A</span>
<MoveRight className="h-3.5 w-3.5" />
<span>Z</span>
</>
) : propertyName === "due_date" || propertyName === "created_on" || propertyName === "updated_on" ? (
<>
<ArrowDownWideNarrow className="h-4 w-4 stroke-[1.5]" />
<span>New</span>
<MoveRight className="h-3.5 w-3.5" />
<span>Old</span>
</>
) : (
<>
<ArrowDownWideNarrow className="h-4 w-4 stroke-[1.5]" />
<span>First</span>
<MoveRight className="h-3.5 w-3.5" />
<span>Last</span>
</>
)}
</div>
<CheckIcon
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
selectedMenuItem === `${ascendingOrder}_${propertyName}` ? "opacity-100" : ""
}`}
/>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
className={`mt-0.5 ${
selectedMenuItem === `${descendingOrder}_${propertyName}` ? "bg-custom-background-80" : ""
}`}
key={propertyName}
onClick={() => {
handleOrderBy(descendingOrder, propertyName);
}}
>
<div
className={`group flex gap-1.5 px-1 items-center justify-between ${
selectedMenuItem === `${descendingOrder}_${propertyName}`
? "text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100"
}`}
>
<div className="flex gap-2 items-center">
{propertyName === "assignee" || propertyName === "labels" ? (
<>
<ArrowUpNarrowWide className="h-4 w-4 stroke-[1.5]" />
<span>Z</span>
<MoveRight className="h-3.5 w-3.5" />
<span>A</span>
</>
) : propertyName === "due_date" ? (
<>
<ArrowUpNarrowWide className="h-4 w-4 stroke-[1.5]" />
<span>Old</span>
<MoveRight className="h-3.5 w-3.5" />
<span>New</span>
</>
) : (
<>
<ArrowUpNarrowWide className="h-4 w-4 stroke-[1.5]" />
<span>Last</span>
<MoveRight className="h-3.5 w-3.5" />
<span>First</span>
</>
)}
</div>
<CheckIcon
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
selectedMenuItem === `${descendingOrder}_${propertyName}` ? "opacity-100" : ""
}`}
/>
</div>
</CustomMenu.MenuItem>
{selectedMenuItem &&
selectedMenuItem !== "" &&
displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(propertyName) && (
<CustomMenu.MenuItem
className={`mt-0.5${
selectedMenuItem === `-created_at_${propertyName}` ? "bg-custom-background-80" : ""
}`}
key={propertyName}
onClick={() => {
handleOrderBy("-created_at", propertyName);
}}
>
<div className={`group flex gap-1.5 px-1 items-center justify-between `}>
<div className="flex gap-1.5 items-center">
<span className="relative flex items-center justify-center h-6 w-6">
<Eraser className="h-3.5 w-3.5" />
</span>
<span>Clear sorting</span>
</div>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
</div>
<div className="h-full min-w-[9rem] w-full">
{issues?.map((issue) => (
<Component
key={issue.id}
issue={issue}
projectId={issue.project_detail.id}
handleUpdateIssue={handleUpdateIssue}
expandedIssues={expandedIssues}
properties={displayProperties}
isNotAllowed={disableUserActions}
/>
))}
</div>
</div>
);
const handleScroll = () => {
if (containerRef.current) {
const scrollLeft = containerRef.current.scrollLeft;
setIsScrolled(scrollLeft > 0);
}
};
useEffect(() => {
const currentContainerRef = containerRef.current;
if (currentContainerRef) {
currentContainerRef.addEventListener("scroll", handleScroll);
}
return () => {
if (currentContainerRef) {
currentContainerRef.removeEventListener("scroll", handleScroll);
}
};
}, []);
return (
<>
<IssuePeekOverview
projectId={currentProjectId ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-200">
<div className="h-full w-full flex flex-col">
<div ref={containerRef} className="flex max-h-full h-full overflow-y-auto">
{issues ? (
<>
<div className="sticky left-0 w-[28rem] z-[2]">
<div
className="relative flex flex-col h-max w-full bg-custom-background-100 z-[2]"
style={{
boxShadow: isScrolled ? "8px -9px 12px rgba(0, 0, 0, 0.15)" : "",
}}
>
<div className="flex items-center text-sm font-medium z-[2] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-100">
{displayProperties.key && (
<span className="flex items-center px-4 py-2.5 h-full w-24 flex-shrink-0">ID</span>
)}
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">Issue</span>
</div>
{issues.map((issue: IIssue, index) => (
<SpreadsheetIssuesColumn
key={`${issue.id}_${index}`}
issue={issue}
projectId={issue.project_detail.id}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
setCurrentProjectId={setCurrentProjectId}
properties={displayProperties}
handleIssueAction={handleIssueAction}
disableUserActions={disableUserActions}
/>
))}
</div>
</div>
{displayProperties.state &&
renderColumn("State", "state", SpreadsheetStateColumn, "state__name", "-state__name")}
{displayProperties.priority &&
renderColumn("Priority", "priority", SpreadsheetPriorityColumn, "priority", "-priority")}
{displayProperties.assignee &&
renderColumn(
"Assignees",
"assignee",
SpreadsheetAssigneeColumn,
"assignees__first_name",
"-assignees__first_name"
)}
{displayProperties.labels &&
renderColumn("Label", "labels", SpreadsheetLabelColumn, "labels__name", "-labels__name")}
{displayProperties.start_date &&
renderColumn("Start Date", "start_date", SpreadsheetStartDateColumn, "-start_date", "start_date")}
{displayProperties.due_date &&
renderColumn("Due Date", "due_date", SpreadsheetDueDateColumn, "-target_date", "target_date")}
{displayProperties.estimate &&
renderColumn("Estimate", "estimate", SpreadsheetEstimateColumn, "estimate_point", "-estimate_point")}
{displayProperties.created_on &&
renderColumn("Created On", "created_on", SpreadsheetCreatedOnColumn, "-created_at", "created_at")}
{displayProperties.updated_on &&
renderColumn("Updated On", "updated_on", SpreadsheetUpdatedOnColumn, "-updated_at", "updated_at")}
</>
) : (
<div className="flex flex-col justify-center items-center h-full w-full">
<Spinner />
</div>
)}
</div>
<div className="border-t border-custom-border-100">
<div className="mb-3 z-50 sticky bottom-0 left-0">
{/* <ListInlineCreateIssueForm
isOpen={isInlineCreateIssueFormOpen}
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/> */}
</div>
{type === "issue"
? !disableUserActions &&
!isInlineCreateIssueFormOpen && (
<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>
)
: !disableUserActions &&
!isInlineCreateIssueFormOpen && (
<CustomMenu
className="sticky left-0 z-10"
customButton={
<button
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
New Issue
</button>
}
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
</div>
</div>
</>
);
});

View file

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

View file

@ -1,62 +0,0 @@
import React from "react";
// components
import { StartDateColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetStartDateColumn: React.FC<Props> = ({
issue,
projectId,
partialUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<StartDateColumn
issue={issue}
projectId={projectId}
properties={properties}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetStartDateColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
partialUpdateIssue={partialUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

@ -1,33 +0,0 @@
import React from "react";
// components
import { ViewStartDateSelect } from "components/issues";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const StartDateColumn: React.FC<Props> = (props) => {
const { issue, partialUpdateIssue, properties, user, isNotAllowed } = props;
return (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.due_date && (
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
)}
</span>
</div>
);
};

View file

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

View file

@ -1,64 +0,0 @@
import React from "react";
// components
import { StateColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
handleUpdateIssue: (issueId: string, data: Partial<IIssue>) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetStateColumn: React.FC<Props> = ({
issue,
projectId,
handleUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
{user && (
<StateColumn
issue={issue}
projectId={projectId}
properties={properties}
onChange={(data) => handleUpdateIssue(issue.id, data)}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue) => (
<SpreadsheetStateColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
handleUpdateIssue={handleUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

@ -1,77 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
// components
import { StateSelect } from "components/states";
// services
import { TrackEventService } from "services/track_event.service";
// types
import { IUser, IIssue, IState, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
onChange: (formData: Partial<IIssue>) => void;
properties: Properties;
user: IUser;
isNotAllowed: boolean;
};
const trackEventService = new TrackEventService();
export const StateColumn: React.FC<Props> = ({ issue, projectId, onChange, properties, user, isNotAllowed }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
onChange({
state: data,
state_detail: newState,
});
trackEventService.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventService.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user
);
}
};
return (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.state && (
<StateSelect
value={issue.state_detail}
projectId={projectId}
onChange={handleStateChange}
buttonClassName="!shadow-none !border-0"
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
</span>
</div>
);
};

View file

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

View file

@ -1,62 +0,0 @@
import React from "react";
// components
import { UpdatedOnColumn } from "components/core";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
import { IUser, IIssue, Properties } from "types";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
expandedIssues: string[];
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = ({
issue,
projectId,
partialUpdateIssue,
expandedIssues,
properties,
user,
isNotAllowed,
}) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div>
<UpdatedOnColumn
issue={issue}
projectId={projectId}
properties={properties}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
{isExpanded &&
!isLoading &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssue: IIssue) => (
<SpreadsheetUpdatedOnColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
partialUpdateIssue={partialUpdateIssue}
expandedIssues={expandedIssues}
properties={properties}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
);
};

View file

@ -1,30 +0,0 @@
import { FC } from "react";
// types
import { IUser, IIssue, Properties } from "types";
// helper
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
type Props = {
issue: IIssue;
projectId: string;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
properties: Properties;
user: IUser | undefined;
isNotAllowed: boolean;
};
export const UpdatedOnColumn: FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { issue, projectId, partialUpdateIssue, properties, user, isNotAllowed } = props;
return (
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
{properties.updated_on && (
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{renderLongDetailDateFormat(issue.updated_at)}
</div>
)}
</span>
</div>
);
};