mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
style: spreadsheet view revamp and code refactor
This commit is contained in:
parent
372074fce1
commit
19a28ea9d5
@ -0,0 +1,72 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { MembersSelect } from "components/project";
|
||||||
|
// services
|
||||||
|
import trackEventServices from "services/track-event.service";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AssigneeColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
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);
|
||||||
|
|
||||||
|
partialUpdateIssue({ assignees_list: data }, issue);
|
||||||
|
|
||||||
|
trackEventServices.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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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-200">
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-assignee-column";
|
||||||
|
export * from "./assignee-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { AssigneeColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetAssigneeColumn: 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>
|
||||||
|
<AssigneeColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
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}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, 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: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreatedOnColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<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-200">
|
||||||
|
{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>
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-created-on-column";
|
||||||
|
export * from "./created-on-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { CreatedOnColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetCreatedOnColumn: 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>
|
||||||
|
<CreatedOnColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetCreatedOnColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { ViewDueDateSelect } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DueDateColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<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-200">
|
||||||
|
{properties.due_date && (
|
||||||
|
<ViewDueDateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-due-date-column";
|
||||||
|
export * from "./due-date-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { DueDateColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | 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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { ViewEstimateSelect } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimateColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<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-200">
|
||||||
|
{properties.estimate && (
|
||||||
|
<ViewEstimateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-estimate-column";
|
||||||
|
export * from "./estimate-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { EstimateColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,14 @@
|
|||||||
|
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 "./spreadsheet-view";
|
||||||
export * from "./single-issue";
|
export * from "./issue-column/issue-column";
|
||||||
export * from "./spreadsheet-columns";
|
export * from "./spreadsheet-columns";
|
||||||
export * from "./spreadsheet-issues";
|
export * from "./issue-column/spreadsheet-issue-column";
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-issue-column";
|
||||||
|
export * from "./issue-column";
|
@ -0,0 +1,179 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Popover2 } from "@blueprintjs/popover2";
|
||||||
|
// icons
|
||||||
|
import { Icon } from "components/ui";
|
||||||
|
import {
|
||||||
|
EllipsisHorizontalIcon,
|
||||||
|
LinkIcon,
|
||||||
|
PencilIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// types
|
||||||
|
import { IIssue, Properties, UserAuth } 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;
|
||||||
|
userAuth: UserAuth;
|
||||||
|
nestingLevel: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
expanded,
|
||||||
|
handleToggleExpand,
|
||||||
|
properties,
|
||||||
|
handleEditIssue,
|
||||||
|
handleDeleteIssue,
|
||||||
|
setCurrentProjectId,
|
||||||
|
disableUserActions,
|
||||||
|
userAuth,
|
||||||
|
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`;
|
||||||
|
|
||||||
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
|
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-200 ">
|
||||||
|
<div
|
||||||
|
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center"
|
||||||
|
style={issue.parent ? { paddingLeft } : {}}
|
||||||
|
>
|
||||||
|
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
|
||||||
|
{properties.key && (
|
||||||
|
<span className="flex items-center justify-center font-medium 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">
|
||||||
|
<PencilIcon 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">
|
||||||
|
<TrashIcon 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"
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon 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)}
|
||||||
|
>
|
||||||
|
<Icon iconName="chevron_right" className={`${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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,40 +1,34 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { SingleSpreadsheetIssue } from "components/core";
|
import { IssueColumn } from "components/core";
|
||||||
// hooks
|
// hooks
|
||||||
import useSubIssue from "hooks/use-sub-issue";
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
import { IIssue, Properties, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
index: number;
|
|
||||||
expandedIssues: string[];
|
expandedIssues: string[];
|
||||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
gridTemplateColumns: string;
|
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
nestingLevel?: number;
|
nestingLevel?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetIssues: React.FC<Props> = ({
|
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
||||||
index,
|
|
||||||
issue,
|
issue,
|
||||||
projectId,
|
projectId,
|
||||||
expandedIssues,
|
expandedIssues,
|
||||||
setExpandedIssues,
|
setExpandedIssues,
|
||||||
gridTemplateColumns,
|
|
||||||
properties,
|
properties,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
setCurrentProjectId,
|
setCurrentProjectId,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
user,
|
|
||||||
userAuth,
|
userAuth,
|
||||||
nestingLevel = 0,
|
nestingLevel = 0,
|
||||||
}) => {
|
}) => {
|
||||||
@ -57,19 +51,16 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SingleSpreadsheetIssue
|
<IssueColumn
|
||||||
issue={issue}
|
issue={issue}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
index={index}
|
|
||||||
expanded={isExpanded}
|
expanded={isExpanded}
|
||||||
handleToggleExpand={handleToggleExpand}
|
handleToggleExpand={handleToggleExpand}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
setCurrentProjectId={setCurrentProjectId}
|
setCurrentProjectId={setCurrentProjectId}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
nestingLevel={nestingLevel}
|
nestingLevel={nestingLevel}
|
||||||
/>
|
/>
|
||||||
@ -79,19 +70,16 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetIssues
|
<SpreadsheetIssuesColumn
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
issue={subIssue}
|
issue={subIssue}
|
||||||
projectId={subIssue.project_detail.id}
|
projectId={subIssue.project_detail.id}
|
||||||
index={index}
|
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
setCurrentProjectId={setCurrentProjectId}
|
setCurrentProjectId={setCurrentProjectId}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
nestingLevel={nestingLevel + 1}
|
nestingLevel={nestingLevel + 1}
|
||||||
/>
|
/>
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-label-column";
|
||||||
|
export * from "./label-column";
|
@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { LabelSelect } from "components/project";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | 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-200">
|
||||||
|
{properties.labels && (
|
||||||
|
<LabelSelect
|
||||||
|
value={issue.labels}
|
||||||
|
projectId={projectId}
|
||||||
|
onChange={handleLabelChange}
|
||||||
|
labelsDetails={issue.label_details}
|
||||||
|
hideDropdownArrow
|
||||||
|
maxRender={1}
|
||||||
|
user={user}
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { LabelColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | 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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-priority-column";
|
||||||
|
export * from "./priority-column";
|
@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { PrioritySelect } from "components/project";
|
||||||
|
// services
|
||||||
|
import trackEventServices from "services/track-event.service";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties, TIssuePriorities } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PriorityColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||||
|
partialUpdateIssue({ priority: data }, issue);
|
||||||
|
trackEventServices.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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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-200">
|
||||||
|
{properties.priority && (
|
||||||
|
<PrioritySelect
|
||||||
|
value={issue.priority}
|
||||||
|
onChange={handlePriorityChange}
|
||||||
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { PriorityColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,522 +0,0 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { mutate } from "swr";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
|
||||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
|
||||||
import { StateSelect } from "components/states";
|
|
||||||
import { Popover2 } from "@blueprintjs/popover2";
|
|
||||||
// icons
|
|
||||||
import { Icon } from "components/ui";
|
|
||||||
import {
|
|
||||||
EllipsisHorizontalIcon,
|
|
||||||
LinkIcon,
|
|
||||||
PencilIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// hooks
|
|
||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
|
|
||||||
// services
|
|
||||||
import issuesService from "services/issues.service";
|
|
||||||
import trackEventServices 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,
|
|
||||||
WORKSPACE_VIEW_ISSUES,
|
|
||||||
} from "constants/fetch-keys";
|
|
||||||
// types
|
|
||||||
import {
|
|
||||||
ICurrentUserResponse,
|
|
||||||
IIssue,
|
|
||||||
IState,
|
|
||||||
ISubIssueResponse,
|
|
||||||
Properties,
|
|
||||||
TIssuePriorities,
|
|
||||||
UserAuth,
|
|
||||||
} from "types";
|
|
||||||
// helper
|
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
|
||||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issue: IIssue;
|
|
||||||
projectId: string;
|
|
||||||
index: number;
|
|
||||||
expanded: boolean;
|
|
||||||
handleToggleExpand: (issueId: string) => void;
|
|
||||||
properties: Properties;
|
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
|
||||||
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
|
||||||
gridTemplateColumns: string;
|
|
||||||
disableUserActions: boolean;
|
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
userAuth: UserAuth;
|
|
||||||
nestingLevel: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|
||||||
issue,
|
|
||||||
projectId,
|
|
||||||
index,
|
|
||||||
expanded,
|
|
||||||
handleToggleExpand,
|
|
||||||
properties,
|
|
||||||
handleEditIssue,
|
|
||||||
handleDeleteIssue,
|
|
||||||
setCurrentProjectId,
|
|
||||||
gridTemplateColumns,
|
|
||||||
disableUserActions,
|
|
||||||
user,
|
|
||||||
userAuth,
|
|
||||||
nestingLevel,
|
|
||||||
}) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { workspaceSlug, cycleId, moduleId, viewId, workspaceViewId } = router.query;
|
|
||||||
|
|
||||||
const { params } = useSpreadsheetIssuesView();
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const workspaceIssuesPath = [
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
sub_issue: false,
|
|
||||||
},
|
|
||||||
path: "workspace-views/all-issues",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
assignees: user?.id ?? undefined,
|
|
||||||
sub_issue: false,
|
|
||||||
},
|
|
||||||
path: "workspace-views/assigned",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
created_by: user?.id ?? undefined,
|
|
||||||
sub_issue: false,
|
|
||||||
},
|
|
||||||
path: "workspace-views/created",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
subscriber: user?.id ?? undefined,
|
|
||||||
sub_issue: false,
|
|
||||||
},
|
|
||||||
path: "workspace-views/subscribed",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentWorkspaceIssuePath = workspaceIssuesPath.find((path) =>
|
|
||||||
router.pathname.includes(path.path)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { params: workspaceViewParams } = useWorkspaceIssuesFilters(
|
|
||||||
workspaceSlug?.toString(),
|
|
||||||
workspaceViewId?.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
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)
|
|
||||||
: workspaceViewId
|
|
||||||
? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), workspaceViewParams)
|
|
||||||
: currentWorkspaceIssuePath
|
|
||||||
? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.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
|
|
||||||
);
|
|
||||||
|
|
||||||
issuesService
|
|
||||||
.patchIssue(workspaceSlug as string, projectId, issue.id as string, formData, user)
|
|
||||||
.then(() => {
|
|
||||||
if (issue.parent) {
|
|
||||||
mutate(SUB_ISSUES(issue.parent as string));
|
|
||||||
} else {
|
|
||||||
mutate(fetchKey);
|
|
||||||
|
|
||||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
|
||||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[
|
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
cycleId,
|
|
||||||
moduleId,
|
|
||||||
viewId,
|
|
||||||
workspaceViewId,
|
|
||||||
currentWorkspaceIssuePath,
|
|
||||||
workspaceViewParams,
|
|
||||||
params,
|
|
||||||
user,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
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 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
|
|
||||||
);
|
|
||||||
trackEventServices.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") {
|
|
||||||
trackEventServices.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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
|
||||||
partialUpdateIssue({ priority: data }, issue);
|
|
||||||
trackEventServices.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
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
trackEventServices.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
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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">
|
|
||||||
<PencilIcon 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">
|
|
||||||
<TrashIcon 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"
|
|
||||||
>
|
|
||||||
<EllipsisHorizontalIcon 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)}
|
|
||||||
>
|
|
||||||
<Icon iconName="chevron_right" className={`${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">
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,22 +1,43 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { KeyedMutator } from "swr";
|
import { KeyedMutator, mutate } from "swr";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
import {
|
||||||
import { CustomMenu, Spinner } from "components/ui";
|
SpreadsheetAssigneeColumn,
|
||||||
|
SpreadsheetCreatedOnColumn,
|
||||||
|
SpreadsheetDueDateColumn,
|
||||||
|
SpreadsheetEstimateColumn,
|
||||||
|
SpreadsheetIssuesColumn,
|
||||||
|
SpreadsheetLabelColumn,
|
||||||
|
SpreadsheetPriorityColumn,
|
||||||
|
SpreadsheetStartDateColumn,
|
||||||
|
SpreadsheetStateColumn,
|
||||||
|
SpreadsheetUpdatedOnColumn,
|
||||||
|
} from "components/core";
|
||||||
|
import { Spinner } from "components/ui";
|
||||||
import { IssuePeekOverview } from "components/issues";
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types";
|
||||||
// constants
|
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
|
||||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
import {
|
||||||
|
CYCLE_DETAILS,
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_DETAILS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
SUB_ISSUES,
|
||||||
|
VIEW_ISSUES,
|
||||||
|
WORKSPACE_VIEW_ISSUES,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
import projectIssuesServices from "services/issues.service";
|
||||||
// icon
|
// icon
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
spreadsheetIssues: IIssue[];
|
spreadsheetIssues: IIssue[];
|
||||||
@ -46,27 +67,162 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query;
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
|
||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
const workspaceIssuesPath = [
|
||||||
...column,
|
{
|
||||||
isActive: properties
|
params: {
|
||||||
? column.propertyName === "labels"
|
sub_issue: false,
|
||||||
? properties[column.propertyName as keyof Properties]
|
},
|
||||||
: column.propertyName === "title"
|
path: "workspace-views/all-issues",
|
||||||
? true
|
},
|
||||||
: properties[column.propertyName as keyof Properties]
|
{
|
||||||
: false,
|
params: {
|
||||||
}));
|
assignees: user?.id ?? undefined,
|
||||||
|
sub_issue: false,
|
||||||
|
},
|
||||||
|
path: "workspace-views/assigned",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
created_by: user?.id ?? undefined,
|
||||||
|
sub_issue: false,
|
||||||
|
},
|
||||||
|
path: "workspace-views/created",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
subscriber: user?.id ?? undefined,
|
||||||
|
sub_issue: false,
|
||||||
|
},
|
||||||
|
path: "workspace-views/subscribed",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const gridTemplateColumns = columnData
|
const currentWorkspaceIssuePath = workspaceIssuesPath.find((path) =>
|
||||||
.filter((column) => column.isActive)
|
router.pathname.includes(path.path)
|
||||||
.map((column) => column.colSize)
|
);
|
||||||
.join(" ");
|
|
||||||
|
const { params: workspaceViewParams } = useWorkspaceIssuesFilters(
|
||||||
|
workspaceSlug?.toString(),
|
||||||
|
workspaceViewId?.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const { params } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
|
const partialUpdateIssue = useCallback(
|
||||||
|
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||||
|
if (!workspaceSlug || !issue) 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)
|
||||||
|
: workspaceViewId
|
||||||
|
? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), workspaceViewParams)
|
||||||
|
: currentWorkspaceIssuePath
|
||||||
|
? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.params)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project_detail.id, 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
|
||||||
|
);
|
||||||
|
|
||||||
|
projectIssuesServices
|
||||||
|
.patchIssue(
|
||||||
|
workspaceSlug as string,
|
||||||
|
issue.project_detail.id,
|
||||||
|
issue.id as string,
|
||||||
|
formData,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (issue.parent) {
|
||||||
|
mutate(SUB_ISSUES(issue.parent as string));
|
||||||
|
} else {
|
||||||
|
mutate(fetchKey);
|
||||||
|
|
||||||
|
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||||
|
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
workspaceSlug,
|
||||||
|
cycleId,
|
||||||
|
moduleId,
|
||||||
|
viewId,
|
||||||
|
workspaceViewId,
|
||||||
|
currentWorkspaceIssuePath,
|
||||||
|
workspaceViewParams,
|
||||||
|
params,
|
||||||
|
user,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
|
const renderColumn = (header: string, Component: React.ComponentType<any>) => (
|
||||||
|
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm">
|
||||||
|
<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-200">
|
||||||
|
{header}
|
||||||
|
</div>
|
||||||
|
<div className="h-full min-w-[9rem] w-full">
|
||||||
|
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||||
|
<Component
|
||||||
|
key={`${issue.id}_${index}`}
|
||||||
|
issue={issue}
|
||||||
|
projectId={issue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -76,79 +232,46 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
readOnly={disableUserActions}
|
readOnly={disableUserActions}
|
||||||
/>
|
/>
|
||||||
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||||
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
|
|
||||||
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
|
||||||
</div>
|
|
||||||
{spreadsheetIssues ? (
|
{spreadsheetIssues ? (
|
||||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
<>
|
||||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
<div className="sticky left-0 w-[28rem] z-[2]">
|
||||||
<SpreadsheetIssues
|
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm z-[2]">
|
||||||
key={`${issue.id}_${index}`}
|
<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-200">
|
||||||
index={index}
|
<span className="flex items-center px-4 py-2.5 h-full w-20 flex-shrink-0">
|
||||||
issue={issue}
|
ID
|
||||||
projectId={issue.project_detail.id}
|
</span>
|
||||||
expandedIssues={expandedIssues}
|
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">
|
||||||
setExpandedIssues={setExpandedIssues}
|
Issue
|
||||||
setCurrentProjectId={setCurrentProjectId}
|
</span>
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
</div>
|
||||||
properties={properties}
|
|
||||||
handleIssueAction={handleIssueAction}
|
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||||
disableUserActions={disableUserActions}
|
<SpreadsheetIssuesColumn
|
||||||
user={user}
|
key={`${issue.id}_${index}`}
|
||||||
userAuth={userAuth}
|
issue={issue}
|
||||||
/>
|
projectId={issue.project_detail.id}
|
||||||
))}
|
expandedIssues={expandedIssues}
|
||||||
<div
|
setExpandedIssues={setExpandedIssues}
|
||||||
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"
|
setCurrentProjectId={setCurrentProjectId}
|
||||||
style={{ gridTemplateColumns }}
|
properties={properties}
|
||||||
>
|
handleIssueAction={handleIssueAction}
|
||||||
{type === "issue" ? (
|
disableUserActions={disableUserActions}
|
||||||
<button
|
userAuth={userAuth}
|
||||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] 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"
|
/>
|
||||||
onClick={() => {
|
))}
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
</div>
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
!disableUserActions && (
|
|
||||||
<CustomMenu
|
|
||||||
className="sticky left-0 z-[1]"
|
|
||||||
customButton={
|
|
||||||
<button
|
|
||||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] 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"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
position="left"
|
|
||||||
optionsClassName="left-5 !w-36"
|
|
||||||
noBorder
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create new
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{openIssuesListModal && (
|
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
|
||||||
Add an existing issue
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{renderColumn("State", SpreadsheetStateColumn)}
|
||||||
|
{renderColumn("Priority", SpreadsheetPriorityColumn)}
|
||||||
|
{renderColumn("Assignees", SpreadsheetAssigneeColumn)}
|
||||||
|
{renderColumn("Label", SpreadsheetLabelColumn)}
|
||||||
|
{renderColumn("Start Date", SpreadsheetStartDateColumn)}
|
||||||
|
{renderColumn("Due Date", SpreadsheetDueDateColumn)}
|
||||||
|
{renderColumn("Estimate", SpreadsheetEstimateColumn)}
|
||||||
|
{renderColumn("Created On", SpreadsheetCreatedOnColumn)}
|
||||||
|
{renderColumn("Updated On", SpreadsheetUpdatedOnColumn)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col justify-center items-center h-full w-full">
|
<div className="flex flex-col justify-center items-center h-full w-full">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-start-date-column";
|
||||||
|
export * from "./start-date-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { StartDateColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | 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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { ViewStartDateSelect } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StartDateColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<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-200">
|
||||||
|
{properties.due_date && (
|
||||||
|
<ViewStartDateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-state-column";
|
||||||
|
export * from "./state-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { StateColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetStateColumn: 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>
|
||||||
|
<StateColumn
|
||||||
|
issue={issue}
|
||||||
|
projectId={projectId}
|
||||||
|
properties={properties}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetStateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
projectId={subIssue.project_detail.id}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
properties={properties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,87 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { StateSelect } from "components/states";
|
||||||
|
// services
|
||||||
|
import trackEventServices from "services/track-event.service";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, IState, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StateColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
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);
|
||||||
|
|
||||||
|
partialUpdateIssue(
|
||||||
|
{
|
||||||
|
state: data,
|
||||||
|
state_detail: newState,
|
||||||
|
},
|
||||||
|
issue
|
||||||
|
);
|
||||||
|
trackEventServices.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") {
|
||||||
|
trackEventServices.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-200">
|
||||||
|
{properties.state && (
|
||||||
|
<StateSelect
|
||||||
|
value={issue.state_detail}
|
||||||
|
projectId={projectId}
|
||||||
|
onChange={handleStateChange}
|
||||||
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-updated-on-column";
|
||||||
|
export * from "./updated-on-column";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { UpdatedOnColumn } from "components/core";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
properties: Properties;
|
||||||
|
user: ICurrentUserResponse | 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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { ICurrentUserResponse, 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: ICurrentUserResponse | undefined;
|
||||||
|
isNotAllowed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdatedOnColumn: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
projectId,
|
||||||
|
partialUpdateIssue,
|
||||||
|
properties,
|
||||||
|
user,
|
||||||
|
isNotAllowed,
|
||||||
|
}) => (
|
||||||
|
<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-200">
|
||||||
|
{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>
|
||||||
|
);
|
@ -1,86 +0,0 @@
|
|||||||
import {
|
|
||||||
CalendarDaysIcon,
|
|
||||||
PlayIcon,
|
|
||||||
Squares2X2Icon,
|
|
||||||
TagIcon,
|
|
||||||
UserGroupIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
export const SPREADSHEET_COLUMN = [
|
|
||||||
{
|
|
||||||
propertyName: "title",
|
|
||||||
colName: "Title",
|
|
||||||
colSize: "440px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "state",
|
|
||||||
colName: "State",
|
|
||||||
colSize: "128px",
|
|
||||||
icon: Squares2X2Icon,
|
|
||||||
ascendingOrder: "state__name",
|
|
||||||
descendingOrder: "-state__name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "priority",
|
|
||||||
colName: "Priority",
|
|
||||||
colSize: "128px",
|
|
||||||
ascendingOrder: "priority",
|
|
||||||
descendingOrder: "-priority",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "assignee",
|
|
||||||
colName: "Assignees",
|
|
||||||
colSize: "128px",
|
|
||||||
icon: UserGroupIcon,
|
|
||||||
ascendingOrder: "assignees__id",
|
|
||||||
descendingOrder: "-assignees__id",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "labels",
|
|
||||||
colName: "Labels",
|
|
||||||
colSize: "128px",
|
|
||||||
icon: TagIcon,
|
|
||||||
ascendingOrder: "labels__name",
|
|
||||||
descendingOrder: "-labels__name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "start_date",
|
|
||||||
colName: "Start Date",
|
|
||||||
colSize: "128px",
|
|
||||||
icon: CalendarDaysIcon,
|
|
||||||
ascendingOrder: "-start_date",
|
|
||||||
descendingOrder: "start_date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "due_date",
|
|
||||||
colName: "Due Date",
|
|
||||||
colSize: "128px",
|
|
||||||
icon: CalendarDaysIcon,
|
|
||||||
ascendingOrder: "-target_date",
|
|
||||||
descendingOrder: "target_date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "estimate",
|
|
||||||
colName: "Estimate",
|
|
||||||
colSize: "128px",
|
|
||||||
icon: PlayIcon,
|
|
||||||
ascendingOrder: "estimate_point",
|
|
||||||
descendingOrder: "-estimate_point",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "created_on",
|
|
||||||
colName: "Created On",
|
|
||||||
colSize: "144px",
|
|
||||||
icon: CalendarDaysIcon,
|
|
||||||
ascendingOrder: "-created_at",
|
|
||||||
descendingOrder: "created_at",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
propertyName: "updated_on",
|
|
||||||
colName: "Updated On",
|
|
||||||
colSize: "144px",
|
|
||||||
icon: CalendarDaysIcon,
|
|
||||||
ascendingOrder: "-updated_at",
|
|
||||||
descendingOrder: "updated_at",
|
|
||||||
},
|
|
||||||
];
|
|
Loading…
Reference in New Issue
Block a user