forked from github/plane
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:
parent
e9cc578cca
commit
3197dd484c
@ -1,3 +1,2 @@
|
|||||||
export * from "./spreadsheet-view";
|
|
||||||
export * from "./issues-view";
|
export * from "./issues-view";
|
||||||
export * from "./inline-issue-create-wrapper";
|
export * from "./inline-issue-create-wrapper";
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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";
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -6,10 +6,9 @@ import useSWR from "swr";
|
|||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetView } from "components/core";
|
import { GlobalViewsAppliedFiltersRoot, SpreadsheetView } from "components/issues";
|
||||||
import { GlobalViewsAppliedFiltersRoot } from "components/issues";
|
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, TStaticViewTypes } from "types";
|
import { IIssue, IIssueDisplayFilterOptions, TStaticViewTypes } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { GLOBAL_VIEW_ISSUES } from "constants/fetch-keys";
|
import { GLOBAL_VIEW_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
@ -28,6 +27,7 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
|
|||||||
globalViewIssues: globalViewIssuesStore,
|
globalViewIssues: globalViewIssuesStore,
|
||||||
globalViewFilters: globalViewFiltersStore,
|
globalViewFilters: globalViewFiltersStore,
|
||||||
workspaceFilter: workspaceFilterStore,
|
workspaceFilter: workspaceFilterStore,
|
||||||
|
workspace: workspaceStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined;
|
const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined;
|
||||||
@ -63,6 +63,18 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
|
|||||||
[workspaceFilterStore, workspaceSlug]
|
[workspaceFilterStore, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleUpdateIssue = useCallback(
|
||||||
|
(issue: IIssue, data: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
console.log("issue", issue);
|
||||||
|
console.log("data", data);
|
||||||
|
|
||||||
|
// TODO: add update issue logic here
|
||||||
|
},
|
||||||
|
[workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
const issues = type
|
const issues = type
|
||||||
? globalViewIssuesStore.viewIssues?.[type]
|
? globalViewIssuesStore.viewIssues?.[type]
|
||||||
: globalViewId
|
: globalViewId
|
||||||
@ -78,8 +90,10 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
|
|||||||
displayFilters={workspaceFilterStore.workspaceDisplayFilters}
|
displayFilters={workspaceFilterStore.workspaceDisplayFilters}
|
||||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
issues={issues}
|
issues={issues}
|
||||||
|
members={workspaceStore.workspaceMembers ? workspaceStore.workspaceMembers.map((m) => m.member) : undefined}
|
||||||
|
labels={workspaceStore.workspaceLabels ? workspaceStore.workspaceLabels : undefined}
|
||||||
handleIssueAction={() => {}}
|
handleIssueAction={() => {}}
|
||||||
handleUpdateIssue={() => {}}
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
disableUserActions={false}
|
disableUserActions={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,32 +12,27 @@ import {
|
|||||||
GanttLayout,
|
GanttLayout,
|
||||||
KanBanLayout,
|
KanBanLayout,
|
||||||
ProjectAppliedFiltersRoot,
|
ProjectAppliedFiltersRoot,
|
||||||
SpreadsheetLayout,
|
ProjectSpreadsheetLayout,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
|
|
||||||
export const ProjectLayoutRoot: React.FC = observer(() => {
|
export const ProjectLayoutRoot: React.FC = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query as {
|
const { workspaceSlug, projectId } = router.query;
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
cycleId: string;
|
|
||||||
moduleId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
|
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_ISSUES` : null,
|
workspaceSlug && projectId ? `REVALIDATE_PROJECT_ISSUES_${projectId.toString()}` : null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId) {
|
if (workspaceSlug && projectId) {
|
||||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
|
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
|
||||||
|
|
||||||
await projectStore.fetchProjectStates(workspaceSlug, projectId);
|
await projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString());
|
||||||
await projectStore.fetchProjectLabels(workspaceSlug, projectId);
|
await projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString());
|
||||||
await projectStore.fetchProjectMembers(workspaceSlug, projectId);
|
await projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString());
|
||||||
await projectStore.fetchProjectEstimates(workspaceSlug, projectId);
|
await projectStore.fetchProjectEstimates(workspaceSlug.toString(), projectId.toString());
|
||||||
|
|
||||||
await issueStore.fetchIssues(workspaceSlug, projectId);
|
await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ revalidateOnFocus: false }
|
{ revalidateOnFocus: false }
|
||||||
@ -58,7 +53,7 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
|
|||||||
) : activeLayout === "gantt_chart" ? (
|
) : activeLayout === "gantt_chart" ? (
|
||||||
<GanttLayout />
|
<GanttLayout />
|
||||||
) : activeLayout === "spreadsheet" ? (
|
) : activeLayout === "spreadsheet" ? (
|
||||||
<SpreadsheetLayout />
|
<ProjectSpreadsheetLayout />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
// components
|
||||||
|
import { MembersSelect } from "components/project";
|
||||||
|
// types
|
||||||
|
import { IIssue, IUserLite } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (members: string[]) => void;
|
||||||
|
members: IUserLite[] | undefined;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AssigneeColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, members, disabled } = 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">
|
||||||
|
<MembersSelect
|
||||||
|
value={issue.assignees}
|
||||||
|
onChange={onChange}
|
||||||
|
members={members ?? []}
|
||||||
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={disabled}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { AssigneeColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue, IUserLite } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
members: IUserLite[] | undefined;
|
||||||
|
onChange: (data: Partial<IIssue>) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onChange, expandedIssues, disabled }) => {
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AssigneeColumn
|
||||||
|
issue={issue}
|
||||||
|
members={members}
|
||||||
|
onChange={(data) => onChange({ assignees_list: data })}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue) => (
|
||||||
|
<SpreadsheetAssigneeColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
onChange={onChange}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
members={members}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
// helper
|
||||||
|
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreatedOnColumn: React.FC<Props> = ({ issue }) => (
|
||||||
|
<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">
|
||||||
|
<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,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { CreatedOnColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
expandedIssues: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIssues }) => {
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CreatedOnColumn issue={issue} />
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetCreatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,146 +0,0 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
|
||||||
// components
|
|
||||||
// import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
|
||||||
import { IssuePeekOverview } from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { CustomMenu } from "components/ui";
|
|
||||||
import { Spinner } from "@plane/ui";
|
|
||||||
// icon
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types";
|
|
||||||
import { IIssueUnGroupedStructure } from "store/issue";
|
|
||||||
// constants
|
|
||||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
|
||||||
|
|
||||||
export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { user } = useUser();
|
|
||||||
const { projectDetails } = useProjectDetails();
|
|
||||||
|
|
||||||
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
|
|
||||||
|
|
||||||
const issues = cycleIssueStore.getIssues;
|
|
||||||
const issueDisplayProperties = issueFilterStore.userDisplayProperties;
|
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
|
||||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
|
||||||
display_filters: {
|
|
||||||
...updatedDisplayFilter,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[issueFilterStore, projectId, workspaceSlug]
|
|
||||||
);
|
|
||||||
|
|
||||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
|
||||||
...column,
|
|
||||||
isActive: issueDisplayProperties
|
|
||||||
? column.propertyName === "labels"
|
|
||||||
? issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
|
|
||||||
: column.propertyName === "title"
|
|
||||||
? true
|
|
||||||
: issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
|
|
||||||
: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const gridTemplateColumns = columnData
|
|
||||||
.filter((column) => column.isActive)
|
|
||||||
.map((column) => column.colSize)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IssuePeekOverview
|
|
||||||
projectId={projectId?.toString() ?? ""}
|
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
|
||||||
readOnly={!isAllowed}
|
|
||||||
/>
|
|
||||||
<div className="h-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}
|
|
||||||
displayFilters={issueFilterStore.userDisplayFilters}
|
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
|
||||||
/> */}
|
|
||||||
</div>
|
|
||||||
{issues ? (
|
|
||||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
|
||||||
{(issues as IIssueUnGroupedStructure).map((issue: IIssue, index) => (
|
|
||||||
<></>
|
|
||||||
// <SpreadsheetIssues
|
|
||||||
// key={`${issue.id}_${index}`}
|
|
||||||
// index={index}
|
|
||||||
// issue={issue}
|
|
||||||
// expandedIssues={expandedIssues}
|
|
||||||
// setExpandedIssues={setExpandedIssues}
|
|
||||||
// gridTemplateColumns={gridTemplateColumns}
|
|
||||||
// properties={issueDisplayProperties}
|
|
||||||
// handleIssueAction={() => {}}
|
|
||||||
// disableUserActions={!isAllowed}
|
|
||||||
// user={user}
|
|
||||||
// userAuth={{
|
|
||||||
// isViewer: projectDetails?.member_role === 5,
|
|
||||||
// isGuest: projectDetails?.member_role === 10,
|
|
||||||
// isMember: projectDetails?.member_role === 15,
|
|
||||||
// isOwner: projectDetails?.member_role === 20,
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
))}
|
|
||||||
<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 }}
|
|
||||||
>
|
|
||||||
{isAllowed && (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
optionsClassName="left-5 !w-36"
|
|
||||||
noBorder
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create new
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{true && <CustomMenu.MenuItem onClick={() => {}}>Add an existing issue</CustomMenu.MenuItem>}
|
|
||||||
</CustomMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Spinner />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -0,0 +1,23 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
// components
|
||||||
|
import { ViewDueDateSelect } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (date: string | null) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DueDateColumn: FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, disabled } = 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">
|
||||||
|
<ViewDueDateSelect issue={issue} onChange={onChange} noBorder disabled={disabled} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { DueDateColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (data: Partial<IIssue>) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DueDateColumn issue={issue} onChange={(val) => onChange({ target_date: val })} disabled={disabled} />
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetDueDateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
onChange={onChange}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
// components
|
||||||
|
import { ViewEstimateSelect } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (data: number) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimateColumn: FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, disabled } = 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">
|
||||||
|
<ViewEstimateSelect issue={issue} onChange={onChange} disabled={disabled} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,41 @@
|
|||||||
|
// components
|
||||||
|
import { EstimateColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (formData: Partial<IIssue>) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, expandedIssues, disabled } = props;
|
||||||
|
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EstimateColumn issue={issue} onChange={(data) => onChange({ estimate_point: data })} disabled={disabled} />
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetEstimateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
onChange={onChange}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,15 @@
|
|||||||
export * from "./cycle-root";
|
export * from "./assignee-column";
|
||||||
export * from "./module-root";
|
export * from "./created-on-column";
|
||||||
export * from "./project-view-root";
|
export * from "./due-date-column";
|
||||||
export * from "./root";
|
export * from "./estimate-column";
|
||||||
|
export * from "./issue-column";
|
||||||
|
export * from "./label-column";
|
||||||
|
export * from "./priority-column";
|
||||||
|
export * from "./roots";
|
||||||
|
export * from "./start-date-column";
|
||||||
|
export * from "./state-column";
|
||||||
|
export * from "./updated-on-column";
|
||||||
|
export * from "./issue-column";
|
||||||
|
export * from "./spreadsheet-column";
|
||||||
|
export * from "./spreadsheet-columns-list";
|
||||||
|
export * from "./spreadsheet-view";
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// components
|
|
||||||
import { Popover2 } from "@blueprintjs/popover2";
|
import { Popover2 } from "@blueprintjs/popover2";
|
||||||
// icons
|
|
||||||
import { MoreHorizontal, LinkIcon, Pencil, Trash2, ChevronRight } from "lucide-react";
|
import { MoreHorizontal, LinkIcon, Pencil, Trash2, ChevronRight } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
// helpers
|
||||||
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, Properties } from "types";
|
import { IIssue, Properties } from "types";
|
||||||
// helper
|
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
@ -21,7 +17,6 @@ type Props = {
|
|||||||
properties: Properties;
|
properties: Properties;
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
nestingLevel: number;
|
nestingLevel: number;
|
||||||
};
|
};
|
||||||
@ -34,7 +29,6 @@ export const IssueColumn: React.FC<Props> = ({
|
|||||||
properties,
|
properties,
|
||||||
handleEditIssue,
|
handleEditIssue,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
setCurrentProjectId,
|
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
nestingLevel,
|
nestingLevel,
|
||||||
}) => {
|
}) => {
|
||||||
@ -48,7 +42,7 @@ export const IssueColumn: React.FC<Props> = ({
|
|||||||
|
|
||||||
const openPeekOverview = () => {
|
const openPeekOverview = () => {
|
||||||
const { query } = router;
|
const { query } = router;
|
||||||
setCurrentProjectId(issue.project_detail.id);
|
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query, peekIssue: issue.id },
|
query: { ...query, peekIssue: issue.id },
|
||||||
@ -87,47 +81,45 @@ export const IssueColumn: React.FC<Props> = ({
|
|||||||
canEscapeKeyClose
|
canEscapeKeyClose
|
||||||
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
||||||
content={
|
content={
|
||||||
<div
|
<div className="flex flex-col whitespace-nowrap rounded-md border border-custom-border-100 p-1 text-xs shadow-lg focus:outline-none min-w-full bg-custom-background-100 space-y-0.5">
|
||||||
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
|
<button
|
||||||
type="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"
|
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleEditIssue(issue);
|
handleEditIssue(issue);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-3 w-3" />
|
||||||
<span>Edit issue</span>
|
<span>Edit issue</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="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"
|
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteIssue(issue);
|
handleDeleteIssue(issue);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-3 w-3" />
|
||||||
<span>Delete issue</span>
|
<span>Delete issue</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="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"
|
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleCopyText();
|
handleCopyText();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LinkIcon className="h-4 w-4" />
|
<LinkIcon className="h-3 w-3" />
|
||||||
<span>Copy issue link</span>
|
<span>Copy issue link</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { IssueColumn } from "components/core";
|
import { IssueColumn } from "components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
import useSubIssue from "hooks/use-sub-issue";
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
// types
|
// types
|
||||||
@ -14,7 +14,6 @@ type Props = {
|
|||||||
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>>;
|
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
nestingLevel?: number;
|
nestingLevel?: number;
|
||||||
};
|
};
|
||||||
@ -26,7 +25,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
setExpandedIssues,
|
setExpandedIssues,
|
||||||
properties,
|
properties,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
setCurrentProjectId,
|
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
nestingLevel = 0,
|
nestingLevel = 0,
|
||||||
}) => {
|
}) => {
|
||||||
@ -34,11 +32,10 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
setExpandedIssues((prevState) => {
|
setExpandedIssues((prevState) => {
|
||||||
const newArray = [...prevState];
|
const newArray = [...prevState];
|
||||||
const index = newArray.indexOf(issueId);
|
const index = newArray.indexOf(issueId);
|
||||||
if (index > -1) {
|
|
||||||
newArray.splice(index, 1);
|
if (index > -1) newArray.splice(index, 1);
|
||||||
} else {
|
else newArray.push(issueId);
|
||||||
newArray.push(issueId);
|
|
||||||
}
|
|
||||||
return newArray;
|
return newArray;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -57,7 +54,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
properties={properties}
|
properties={properties}
|
||||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
setCurrentProjectId={setCurrentProjectId}
|
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
nestingLevel={nestingLevel}
|
nestingLevel={nestingLevel}
|
||||||
/>
|
/>
|
||||||
@ -75,7 +71,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
setCurrentProjectId={setCurrentProjectId}
|
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
nestingLevel={nestingLevel + 1}
|
nestingLevel={nestingLevel + 1}
|
||||||
/>
|
/>
|
@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { LabelSelect } from "components/project";
|
||||||
|
// types
|
||||||
|
import { IIssue, IIssueLabels } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (data: string[]) => void;
|
||||||
|
labels: IIssueLabels[] | undefined;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LabelColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, labels, disabled } = 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">
|
||||||
|
<LabelSelect
|
||||||
|
value={issue.labels}
|
||||||
|
onChange={onChange}
|
||||||
|
labels={labels ?? []}
|
||||||
|
hideDropdownArrow
|
||||||
|
maxRender={1}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { LabelColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue, IIssueLabels } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (formData: Partial<IIssue>) => void;
|
||||||
|
labels: IIssueLabels[] | undefined;
|
||||||
|
expandedIssues: string[];
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, labels, expandedIssues, disabled } = props;
|
||||||
|
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LabelColumn
|
||||||
|
issue={issue}
|
||||||
|
onChange={(data) => onChange({ labels_list: data })}
|
||||||
|
labels={labels}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetLabelColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
onChange={onChange}
|
||||||
|
labels={labels}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,146 +0,0 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
|
||||||
// components
|
|
||||||
// import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
|
||||||
import { IssuePeekOverview } from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { CustomMenu } from "components/ui";
|
|
||||||
import { Spinner } from "@plane/ui";
|
|
||||||
// icon
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types";
|
|
||||||
import { IIssueUnGroupedStructure } from "store/issue";
|
|
||||||
// constants
|
|
||||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
|
||||||
|
|
||||||
export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { user } = useUser();
|
|
||||||
const { projectDetails } = useProjectDetails();
|
|
||||||
|
|
||||||
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
|
|
||||||
|
|
||||||
const issues = moduleIssueStore.getIssues;
|
|
||||||
const issueDisplayProperties = issueFilterStore.userDisplayProperties;
|
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
|
||||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
|
||||||
display_filters: {
|
|
||||||
...updatedDisplayFilter,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[issueFilterStore, projectId, workspaceSlug]
|
|
||||||
);
|
|
||||||
|
|
||||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
|
||||||
...column,
|
|
||||||
isActive: issueDisplayProperties
|
|
||||||
? column.propertyName === "labels"
|
|
||||||
? issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
|
|
||||||
: column.propertyName === "title"
|
|
||||||
? true
|
|
||||||
: issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
|
|
||||||
: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const gridTemplateColumns = columnData
|
|
||||||
.filter((column) => column.isActive)
|
|
||||||
.map((column) => column.colSize)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IssuePeekOverview
|
|
||||||
projectId={projectId?.toString() ?? ""}
|
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
|
||||||
readOnly={!isAllowed}
|
|
||||||
/>
|
|
||||||
<div className="h-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}
|
|
||||||
displayFilters={issueFilterStore.userDisplayFilters}
|
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
|
||||||
/> */}
|
|
||||||
</div>
|
|
||||||
{issues ? (
|
|
||||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
|
||||||
{(issues as IIssueUnGroupedStructure).map((issue: IIssue, index) => (
|
|
||||||
<></>
|
|
||||||
// <SpreadsheetIssues
|
|
||||||
// key={`${issue.id}_${index}`}
|
|
||||||
// index={index}
|
|
||||||
// issue={issue}
|
|
||||||
// expandedIssues={expandedIssues}
|
|
||||||
// setExpandedIssues={setExpandedIssues}
|
|
||||||
// gridTemplateColumns={gridTemplateColumns}
|
|
||||||
// properties={issueDisplayProperties}
|
|
||||||
// handleIssueAction={() => {}}
|
|
||||||
// disableUserActions={!isAllowed}
|
|
||||||
// user={user}
|
|
||||||
// userAuth={{
|
|
||||||
// isViewer: projectDetails?.member_role === 5,
|
|
||||||
// isGuest: projectDetails?.member_role === 10,
|
|
||||||
// isMember: projectDetails?.member_role === 15,
|
|
||||||
// isOwner: projectDetails?.member_role === 20,
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
))}
|
|
||||||
<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 }}
|
|
||||||
>
|
|
||||||
{isAllowed && (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
optionsClassName="left-5 !w-36"
|
|
||||||
noBorder
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create new
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{true && <CustomMenu.MenuItem onClick={() => {}}>Add an existing issue</CustomMenu.MenuItem>}
|
|
||||||
</CustomMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Spinner />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -0,0 +1,28 @@
|
|||||||
|
// components
|
||||||
|
import { PrioritySelect } from "components/project";
|
||||||
|
// types
|
||||||
|
import { IIssue, TIssuePriorities } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (data: TIssuePriorities) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PriorityColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, disabled } = 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">
|
||||||
|
<PrioritySelect
|
||||||
|
value={issue.priority}
|
||||||
|
onChange={onChange}
|
||||||
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { PriorityColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (data: Partial<IIssue>) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PriorityColumn issue={issue} onChange={(data) => onChange({ priority: data })} disabled={disabled} />
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetPriorityColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
onChange={onChange}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,128 +0,0 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
|
||||||
// components
|
|
||||||
// import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
|
||||||
import { IssuePeekOverview } from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { Spinner } from "components/ui";
|
|
||||||
// icon
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types";
|
|
||||||
import { IIssueUnGroupedStructure } from "store/issue";
|
|
||||||
// constants
|
|
||||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
|
||||||
|
|
||||||
export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { user } = useUser();
|
|
||||||
const { projectDetails } = useProjectDetails();
|
|
||||||
|
|
||||||
const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore();
|
|
||||||
|
|
||||||
// const issues = moduleStore.issues || [];
|
|
||||||
const issueDisplayProperties = issueFilterStore.userDisplayProperties;
|
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
|
||||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
|
||||||
display_filters: {
|
|
||||||
...updatedDisplayFilter,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[issueFilterStore, projectId, workspaceSlug]
|
|
||||||
);
|
|
||||||
|
|
||||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
|
||||||
...column,
|
|
||||||
isActive: issueDisplayProperties
|
|
||||||
? column.propertyName === "labels"
|
|
||||||
? issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
|
|
||||||
: column.propertyName === "title"
|
|
||||||
? true
|
|
||||||
: issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
|
|
||||||
: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const gridTemplateColumns = columnData
|
|
||||||
.filter((column) => column.isActive)
|
|
||||||
.map((column) => column.colSize)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IssuePeekOverview
|
|
||||||
projectId={projectId?.toString() ?? ""}
|
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
|
||||||
readOnly={!isAllowed}
|
|
||||||
/>
|
|
||||||
<div className="h-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}
|
|
||||||
displayFilters={issueFilterStore.userDisplayFilters}
|
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
|
||||||
/> */}
|
|
||||||
</div>
|
|
||||||
{/* {issues ? (
|
|
||||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
|
||||||
{(issues as IIssueUnGroupedStructure).map((issue: IIssue, index) => (
|
|
||||||
<SpreadsheetIssues
|
|
||||||
key={`${issue.id}_${index}`}
|
|
||||||
index={index}
|
|
||||||
issue={issue}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
setExpandedIssues={setExpandedIssues}
|
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
properties={issueDisplayProperties}
|
|
||||||
handleIssueAction={() => {}}
|
|
||||||
disableUserActions={!isAllowed}
|
|
||||||
user={user}
|
|
||||||
userAuth={{
|
|
||||||
isViewer: projectDetails?.member_role === 5,
|
|
||||||
isGuest: projectDetails?.member_role === 10,
|
|
||||||
isMember: projectDetails?.member_role === 15,
|
|
||||||
isOwner: projectDetails?.member_role === 20,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<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 }}
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Spinner />
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,161 +0,0 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// hooks
|
|
||||||
import useUser from "hooks/use-user";
|
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
|
||||||
// components
|
|
||||||
// import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
|
||||||
import { IssuePeekOverview } from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { CustomMenu } from "components/ui";
|
|
||||||
import { Spinner } from "@plane/ui";
|
|
||||||
// icon
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types";
|
|
||||||
import { IIssueUnGroupedStructure } from "store/issue";
|
|
||||||
// constants
|
|
||||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
|
||||||
|
|
||||||
export const SpreadsheetLayout: React.FC = observer(() => {
|
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
|
||||||
|
|
||||||
const { user } = useUser();
|
|
||||||
const { projectDetails } = useProjectDetails();
|
|
||||||
|
|
||||||
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore();
|
|
||||||
|
|
||||||
const issues = issueStore.getIssues;
|
|
||||||
const issueDisplayProperties = issueFilterStore.userDisplayProperties;
|
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
|
||||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
|
||||||
display_filters: {
|
|
||||||
...updatedDisplayFilter,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[issueFilterStore, projectId, workspaceSlug]
|
|
||||||
);
|
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
|
||||||
|
|
||||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
|
||||||
...column,
|
|
||||||
isActive: issueDisplayProperties
|
|
||||||
? column.propertyName === "labels"
|
|
||||||
? issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
|
|
||||||
: column.propertyName === "title"
|
|
||||||
? true
|
|
||||||
: issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
|
|
||||||
: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const gridTemplateColumns = columnData
|
|
||||||
.filter((column) => column.isActive)
|
|
||||||
.map((column) => column.colSize)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IssuePeekOverview
|
|
||||||
projectId={projectId?.toString() ?? ""}
|
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
|
||||||
readOnly={!isAllowed}
|
|
||||||
/>
|
|
||||||
<div className="h-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}
|
|
||||||
displayFilters={issueFilterStore.userDisplayFilters}
|
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
|
||||||
/> */}
|
|
||||||
</div>
|
|
||||||
{/* {issues ? (
|
|
||||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
|
||||||
{(issues as IIssueUnGroupedStructure).map((issue: IIssue, index) => (
|
|
||||||
<SpreadsheetIssues
|
|
||||||
key={`${issue.id}_${index}`}
|
|
||||||
index={index}
|
|
||||||
issue={issue}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
setExpandedIssues={setExpandedIssues}
|
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
properties={issueDisplayProperties}
|
|
||||||
handleIssueAction={() => {}}
|
|
||||||
disableUserActions={!isAllowed}
|
|
||||||
user={user}
|
|
||||||
userAuth={{
|
|
||||||
isViewer: projectDetails?.member_role === 5,
|
|
||||||
isGuest: projectDetails?.member_role === 10,
|
|
||||||
isMember: projectDetails?.member_role === 15,
|
|
||||||
isOwner: projectDetails?.member_role === 20,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<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 }}
|
|
||||||
>
|
|
||||||
{type === "issue" ? (
|
|
||||||
<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"
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
isAllowed && (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<Plus 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>
|
|
||||||
{true && <CustomMenu.MenuItem onClick={() => {}}>Add an existing issue</CustomMenu.MenuItem>}
|
|
||||||
</CustomMenu>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Spinner />
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -0,0 +1,71 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetView } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||||
|
// constants
|
||||||
|
import { IIssueUnGroupedStructure } from "store/issue";
|
||||||
|
|
||||||
|
export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
issueFilter: issueFilterStore,
|
||||||
|
cycleIssue: cycleIssueStore,
|
||||||
|
issueDetail: issueDetailStore,
|
||||||
|
project: projectStore,
|
||||||
|
user: userStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const user = userStore.currentUser;
|
||||||
|
const issues = cycleIssueStore.getIssues;
|
||||||
|
|
||||||
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
|
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
...updatedDisplayFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[issueFilterStore, projectId, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateIssue = useCallback(
|
||||||
|
(issue: IIssue, data: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !projectId || !user) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...issue,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
|
||||||
|
cycleIssueStore.updateIssueStructure(null, null, payload);
|
||||||
|
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user);
|
||||||
|
},
|
||||||
|
[issueDetailStore, cycleIssueStore, projectId, user, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpreadsheetView
|
||||||
|
displayProperties={issueFilterStore.userDisplayProperties}
|
||||||
|
displayFilters={issueFilterStore.userDisplayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
|
issues={issues as IIssueUnGroupedStructure}
|
||||||
|
members={projectId ? projectStore.members?.[projectId.toString()]?.map((m) => m.member) : undefined}
|
||||||
|
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined}
|
||||||
|
states={projectId ? projectStore.states?.[projectId.toString()] : undefined}
|
||||||
|
handleIssueAction={() => {}}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
disableUserActions={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./cycle-root";
|
||||||
|
export * from "./module-root";
|
||||||
|
export * from "./project-root";
|
||||||
|
export * from "./project-view-root";
|
@ -0,0 +1,71 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetView } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||||
|
// constants
|
||||||
|
import { IIssueUnGroupedStructure } from "store/issue";
|
||||||
|
|
||||||
|
export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
issueFilter: issueFilterStore,
|
||||||
|
moduleIssue: moduleIssueStore,
|
||||||
|
issueDetail: issueDetailStore,
|
||||||
|
project: projectStore,
|
||||||
|
user: userStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const user = userStore.currentUser;
|
||||||
|
const issues = moduleIssueStore.getIssues;
|
||||||
|
|
||||||
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
|
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
...updatedDisplayFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[issueFilterStore, projectId, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateIssue = useCallback(
|
||||||
|
(issue: IIssue, data: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !projectId || !user) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...issue,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
|
||||||
|
moduleIssueStore.updateIssueStructure(null, null, payload);
|
||||||
|
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user);
|
||||||
|
},
|
||||||
|
[issueDetailStore, moduleIssueStore, projectId, user, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpreadsheetView
|
||||||
|
displayProperties={issueFilterStore.userDisplayProperties}
|
||||||
|
displayFilters={issueFilterStore.userDisplayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
|
issues={issues as IIssueUnGroupedStructure}
|
||||||
|
members={projectId ? projectStore.members?.[projectId.toString()]?.map((m) => m.member) : undefined}
|
||||||
|
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined}
|
||||||
|
states={projectId ? projectStore.states?.[projectId.toString()] : undefined}
|
||||||
|
handleIssueAction={() => {}}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
disableUserActions={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,71 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetView } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||||
|
// constants
|
||||||
|
import { IIssueUnGroupedStructure } from "store/issue";
|
||||||
|
|
||||||
|
export const ProjectSpreadsheetLayout: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
issue: issueStore,
|
||||||
|
issueFilter: issueFilterStore,
|
||||||
|
issueDetail: issueDetailStore,
|
||||||
|
project: projectStore,
|
||||||
|
user: userStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const user = userStore.currentUser;
|
||||||
|
const issues = issueStore.getIssues;
|
||||||
|
|
||||||
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
|
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
...updatedDisplayFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[issueFilterStore, projectId, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateIssue = useCallback(
|
||||||
|
(issue: IIssue, data: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !projectId || !user) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...issue,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
|
||||||
|
issueStore.updateIssueStructure(null, null, payload);
|
||||||
|
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user);
|
||||||
|
},
|
||||||
|
[issueStore, issueDetailStore, projectId, user, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpreadsheetView
|
||||||
|
displayProperties={issueFilterStore.userDisplayProperties}
|
||||||
|
displayFilters={issueFilterStore.userDisplayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
|
issues={issues as IIssueUnGroupedStructure}
|
||||||
|
members={projectId ? projectStore.members?.[projectId.toString()]?.map((m) => m.member) : undefined}
|
||||||
|
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined}
|
||||||
|
states={projectId ? projectStore.states?.[projectId.toString()] : undefined}
|
||||||
|
handleIssueAction={() => {}}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
disableUserActions={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,71 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetView } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||||
|
// constants
|
||||||
|
import { IIssueUnGroupedStructure } from "store/issue";
|
||||||
|
|
||||||
|
export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
issueFilter: issueFilterStore,
|
||||||
|
projectViewIssues: projectViewIssueStore,
|
||||||
|
issueDetail: issueDetailStore,
|
||||||
|
project: projectStore,
|
||||||
|
user: userStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const user = userStore.currentUser;
|
||||||
|
const issues = projectViewIssueStore.getIssues;
|
||||||
|
|
||||||
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
|
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
...updatedDisplayFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[issueFilterStore, projectId, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateIssue = useCallback(
|
||||||
|
(issue: IIssue, data: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !projectId || !user) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...issue,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
|
||||||
|
projectViewIssueStore.updateIssueStructure(null, null, payload);
|
||||||
|
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user);
|
||||||
|
},
|
||||||
|
[issueDetailStore, projectViewIssueStore, projectId, user, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpreadsheetView
|
||||||
|
displayProperties={issueFilterStore.userDisplayProperties}
|
||||||
|
displayFilters={issueFilterStore.userDisplayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
|
issues={issues as IIssueUnGroupedStructure}
|
||||||
|
members={projectId ? projectStore.members?.[projectId.toString()]?.map((m) => m.member) : undefined}
|
||||||
|
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined}
|
||||||
|
states={projectId ? projectStore.states?.[projectId.toString()] : undefined}
|
||||||
|
handleIssueAction={() => {}}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
disableUserActions={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,267 @@
|
|||||||
|
import {
|
||||||
|
ArrowDownWideNarrow,
|
||||||
|
ArrowUpNarrowWide,
|
||||||
|
CheckIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
Eraser,
|
||||||
|
ListFilter,
|
||||||
|
MoveRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
SpreadsheetAssigneeColumn,
|
||||||
|
SpreadsheetCreatedOnColumn,
|
||||||
|
SpreadsheetDueDateColumn,
|
||||||
|
SpreadsheetEstimateColumn,
|
||||||
|
SpreadsheetLabelColumn,
|
||||||
|
SpreadsheetPriorityColumn,
|
||||||
|
SpreadsheetStartDateColumn,
|
||||||
|
SpreadsheetStateColumn,
|
||||||
|
SpreadsheetUpdatedOnColumn,
|
||||||
|
} from "components/issues";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu } from "components/ui";
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
IIssue,
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueLabels,
|
||||||
|
IStateResponse,
|
||||||
|
IUserLite,
|
||||||
|
TIssueOrderByOptions,
|
||||||
|
} from "types";
|
||||||
|
// constants
|
||||||
|
import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
disableUserActions: boolean;
|
||||||
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
|
expandedIssues: string[];
|
||||||
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
|
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||||
|
issues: IIssue[] | undefined;
|
||||||
|
property: string;
|
||||||
|
members?: IUserLite[] | undefined;
|
||||||
|
labels?: IIssueLabels[] | undefined;
|
||||||
|
states?: IStateResponse | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
||||||
|
const {
|
||||||
|
disableUserActions,
|
||||||
|
displayFilters,
|
||||||
|
expandedIssues,
|
||||||
|
handleDisplayFilterUpdate,
|
||||||
|
handleUpdateIssue,
|
||||||
|
issues,
|
||||||
|
property,
|
||||||
|
members,
|
||||||
|
labels,
|
||||||
|
states,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
||||||
|
"spreadsheetViewSorting",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage(
|
||||||
|
"spreadsheetViewActiveSortingProperty",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
||||||
|
handleDisplayFilterUpdate({ order_by: order });
|
||||||
|
|
||||||
|
setSelectedMenuItem(`${order}_${itemKey}`);
|
||||||
|
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const propertyDetails = SPREADSHEET_PROPERTY_DETAILS[property];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col h-max w-full bg-custom-background-100">
|
||||||
|
<div className="flex items-center min-w-[8rem] px-4 py-1 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="flex items-center justify-between gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{activeSortingProperty === property && (
|
||||||
|
<div className="rounded-full flex items-center justify-center h-3.5 w-3.5">
|
||||||
|
<ListFilter className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{propertyDetails.title}
|
||||||
|
</div>
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width="xl"
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
|
||||||
|
<span>{propertyDetails.ascendingOrderTitle}</span>
|
||||||
|
<MoveRight className="h-3 w-3" />
|
||||||
|
<span>{propertyDetails.descendingOrderTitle}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && (
|
||||||
|
<CheckIcon className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
|
||||||
|
<span>{propertyDetails.descendingOrderTitle}</span>
|
||||||
|
<MoveRight className="h-3 w-3" />
|
||||||
|
<span>{propertyDetails.ascendingOrderTitle}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && (
|
||||||
|
<CheckIcon className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
{selectedMenuItem &&
|
||||||
|
selectedMenuItem !== "" &&
|
||||||
|
displayFilters?.order_by !== "-created_at" &&
|
||||||
|
selectedMenuItem.includes(property) && (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
className={`mt-0.5 ${selectedMenuItem === `-created_at_${property}` ? "bg-custom-background-80" : ""}`}
|
||||||
|
key={property}
|
||||||
|
onClick={() => handleOrderBy("-created_at", property)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 px-1">
|
||||||
|
<Eraser className="h-3 w-3" />
|
||||||
|
<span>Clear sorting</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div className="h-full min-w-[8rem] w-full">
|
||||||
|
{issues?.map((issue) => {
|
||||||
|
if (property === "state")
|
||||||
|
return (
|
||||||
|
<SpreadsheetStateColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
|
states={states}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (property === "priority")
|
||||||
|
return (
|
||||||
|
<SpreadsheetPriorityColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (property === "estimate")
|
||||||
|
return (
|
||||||
|
<SpreadsheetEstimateColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (property === "assignee")
|
||||||
|
return (
|
||||||
|
<SpreadsheetAssigneeColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
members={members}
|
||||||
|
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (property === "labels")
|
||||||
|
return (
|
||||||
|
<SpreadsheetLabelColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
labels={labels}
|
||||||
|
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (property === "start_date")
|
||||||
|
return (
|
||||||
|
<SpreadsheetStartDateColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (property === "due_date")
|
||||||
|
return (
|
||||||
|
<SpreadsheetDueDateColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (property === "created_on")
|
||||||
|
return (
|
||||||
|
<SpreadsheetCreatedOnColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (property === "updated_on")
|
||||||
|
return (
|
||||||
|
<SpreadsheetUpdatedOnColumn
|
||||||
|
key={`${property}-${issue.id}`}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
issue={issue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,147 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { SpreadsheetColumn } from "components/issues";
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
IIssue,
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueDisplayProperties,
|
||||||
|
IIssueLabels,
|
||||||
|
IStateResponse,
|
||||||
|
IUserLite,
|
||||||
|
} from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
|
displayProperties: IIssueDisplayProperties;
|
||||||
|
disableUserActions: boolean;
|
||||||
|
expandedIssues: string[];
|
||||||
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
|
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||||
|
issues: IIssue[] | undefined;
|
||||||
|
members?: IUserLite[] | undefined;
|
||||||
|
labels?: IIssueLabels[] | undefined;
|
||||||
|
states?: IStateResponse | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetColumnsList: React.FC<Props> = observer((props) => {
|
||||||
|
const {
|
||||||
|
disableUserActions,
|
||||||
|
displayFilters,
|
||||||
|
displayProperties,
|
||||||
|
expandedIssues,
|
||||||
|
handleDisplayFilterUpdate,
|
||||||
|
handleUpdateIssue,
|
||||||
|
issues,
|
||||||
|
members,
|
||||||
|
labels,
|
||||||
|
states,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{displayProperties.state && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
states={states}
|
||||||
|
property="state"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{displayProperties.priority && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
property="priority"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{displayProperties.assignee && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
members={members}
|
||||||
|
property="assignee"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{displayProperties.labels && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
labels={labels}
|
||||||
|
property="labels"
|
||||||
|
/>
|
||||||
|
)}{" "}
|
||||||
|
{displayProperties.start_date && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
property="start_date"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{displayProperties.due_date && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
property="due_date"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{displayProperties.estimate && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
property="estimate"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{displayProperties.created_on && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
property="created_on"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{displayProperties.updated_on && (
|
||||||
|
<SpreadsheetColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
property="updated_on"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,188 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
SpreadsheetColumnsList,
|
||||||
|
// ListInlineCreateIssueForm,
|
||||||
|
SpreadsheetIssuesColumn,
|
||||||
|
} from "components/issues";
|
||||||
|
import { CustomMenu } from "components/ui";
|
||||||
|
import { Spinner } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
IIssue,
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueDisplayProperties,
|
||||||
|
IIssueLabels,
|
||||||
|
IStateResponse,
|
||||||
|
IUserLite,
|
||||||
|
} from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
displayProperties: IIssueDisplayProperties;
|
||||||
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
|
issues: IIssue[] | undefined;
|
||||||
|
members?: IUserLite[] | undefined;
|
||||||
|
labels?: IIssueLabels[] | undefined;
|
||||||
|
states?: IStateResponse | undefined;
|
||||||
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||||
|
openIssuesListModal?: (() => void) | null;
|
||||||
|
disableUserActions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||||
|
const {
|
||||||
|
displayProperties,
|
||||||
|
displayFilters,
|
||||||
|
handleDisplayFilterUpdate,
|
||||||
|
issues,
|
||||||
|
members,
|
||||||
|
labels,
|
||||||
|
states,
|
||||||
|
handleIssueAction,
|
||||||
|
handleUpdateIssue,
|
||||||
|
openIssuesListModal,
|
||||||
|
disableUserActions,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<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.05)" : "",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
properties={displayProperties}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SpreadsheetColumnsList
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issues={issues}
|
||||||
|
members={members}
|
||||||
|
labels={labels}
|
||||||
|
states={states}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="grid place-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>
|
||||||
|
|
||||||
|
{!disableUserActions &&
|
||||||
|
!isInlineCreateIssueFormOpen &&
|
||||||
|
(type === "issue" ? (
|
||||||
|
<button
|
||||||
|
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] w-full"
|
||||||
|
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
New Issue
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<CustomMenu
|
||||||
|
className="sticky left-0 z-10"
|
||||||
|
customButton={
|
||||||
|
<button
|
||||||
|
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
New Issue
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
optionsClassName="left-5 !w-36"
|
||||||
|
noBorder
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
||||||
|
Create new
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
{openIssuesListModal && (
|
||||||
|
<CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { StartDateColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (formData: Partial<IIssue>) => void;
|
||||||
|
expandedIssues: string[];
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StartDateColumn issue={issue} onChange={(val) => onChange({ start_date: val })} disabled={disabled} />
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetStartDateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
onChange={onChange}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
// components
|
||||||
|
import { ViewStartDateSelect } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (data: string | null) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StartDateColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, disabled } = 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">
|
||||||
|
<ViewStartDateSelect issue={issue} onChange={onChange} noBorder disabled={disabled} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { StateColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue, IStateResponse } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (data: Partial<IIssue>) => void;
|
||||||
|
states: IStateResponse | undefined;
|
||||||
|
expandedIssues: string[];
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, states, expandedIssues, disabled } = props;
|
||||||
|
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StateColumn
|
||||||
|
issue={issue}
|
||||||
|
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
||||||
|
states={states}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue) => (
|
||||||
|
<SpreadsheetStateColumn
|
||||||
|
key={subIssue.id}
|
||||||
|
issue={subIssue}
|
||||||
|
onChange={onChange}
|
||||||
|
states={states}
|
||||||
|
expandedIssues={expandedIssues}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
// components
|
||||||
|
import { StateSelect } from "components/states";
|
||||||
|
// types
|
||||||
|
import { IIssue, IState, IStateResponse } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
onChange: (formData: IState) => void;
|
||||||
|
states: IStateResponse | undefined;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StateColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, onChange, states, disabled } = 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">
|
||||||
|
<StateSelect
|
||||||
|
value={issue.state_detail}
|
||||||
|
onChange={onChange}
|
||||||
|
stateGroups={states}
|
||||||
|
buttonClassName="!shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { UpdatedOnColumn } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
expandedIssues: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue, expandedIssues } = props;
|
||||||
|
|
||||||
|
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||||
|
|
||||||
|
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<UpdatedOnColumn issue={issue} />
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
!isLoading &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssue: IIssue) => (
|
||||||
|
<SpreadsheetUpdatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
// helpers
|
||||||
|
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdatedOnColumn: React.FC<Props> = (props) => {
|
||||||
|
const { issue } = 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">
|
||||||
|
<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,6 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// swr
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
import { TrackEventService } from "services/track_event.service";
|
import { TrackEventService } from "services/track_event.service";
|
||||||
@ -28,181 +30,153 @@ export interface IIssueProperty {
|
|||||||
const issueService = new IssueService();
|
const issueService = new IssueService();
|
||||||
const trackEventService = new TrackEventService();
|
const trackEventService = new TrackEventService();
|
||||||
|
|
||||||
export const IssueProperty: React.FC<IIssueProperty> = ({
|
export const IssueProperty: React.FC<IIssueProperty> = observer(
|
||||||
workspaceSlug,
|
({ workspaceSlug, projectId, parentIssue, issue, user, editable }) => {
|
||||||
projectId,
|
const [properties] = useIssuesProperties(workspaceSlug, projectId);
|
||||||
parentIssue,
|
|
||||||
issue,
|
|
||||||
user,
|
|
||||||
editable,
|
|
||||||
}) => {
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug, projectId);
|
|
||||||
|
|
||||||
const handlePriorityChange = (data: any) => {
|
const { project: projectStore } = useMobxStore();
|
||||||
partialUpdateIssue({ priority: 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_PRIORITY",
|
|
||||||
user as IUser
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
const handlePriorityChange = (data: any) => {
|
||||||
const oldState = states?.find((s) => s.id === issue.state);
|
partialUpdateIssue({ priority: data });
|
||||||
const newState = states?.find((s) => s.id === data);
|
trackEventService.trackIssuePartialPropertyUpdateEvent(
|
||||||
|
|
||||||
partialUpdateIssue({
|
|
||||||
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 as IUser
|
|
||||||
);
|
|
||||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
|
||||||
trackEventService.trackIssueMarkedAsDoneEvent(
|
|
||||||
{
|
{
|
||||||
workspaceSlug: issue.workspace_detail.slug,
|
workspaceSlug,
|
||||||
workspaceId: issue.workspace_detail.id,
|
workspaceId: issue.workspace,
|
||||||
projectId: issue.project_detail.id,
|
projectId: issue.project_detail.id,
|
||||||
projectIdentifier: issue.project_detail.identifier,
|
projectIdentifier: issue.project_detail.identifier,
|
||||||
projectName: issue.project_detail.name,
|
projectName: issue.project_detail.name,
|
||||||
issueId: issue.id,
|
issueId: issue.id,
|
||||||
},
|
},
|
||||||
|
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||||
user as IUser
|
user as IUser
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const handleAssigneeChange = (data: any) => {
|
const handleStateChange = (data: IState) => {
|
||||||
let newData = issue.assignees ?? [];
|
partialUpdateIssue({
|
||||||
|
state: data.id,
|
||||||
|
state_detail: 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_STATE",
|
||||||
|
user as IUser
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (newData && newData.length > 0) {
|
const handleAssigneeChange = (data: string[]) => {
|
||||||
if (newData.includes(data)) newData = newData.splice(newData.indexOf(data), 1);
|
partialUpdateIssue({ assignees_list: data, assignees: data });
|
||||||
else newData = [...newData, data];
|
|
||||||
} else newData = [...newData, data];
|
|
||||||
|
|
||||||
partialUpdateIssue({ assignees_list: data, assignees: 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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
trackEventService.trackIssuePartialPropertyUpdateEvent(
|
const partialUpdateIssue = async (data: Partial<IIssue>) => {
|
||||||
{
|
mutate(
|
||||||
workspaceSlug,
|
workspaceSlug && parentIssue ? SUB_ISSUES(parentIssue.id) : null,
|
||||||
workspaceId: issue.workspace,
|
(elements: any) => {
|
||||||
projectId: issue.project_detail.id,
|
const _elements = { ...elements };
|
||||||
projectIdentifier: issue.project_detail.identifier,
|
const _issues = _elements.sub_issues.map((element: IIssue) =>
|
||||||
projectName: issue.project_detail.name,
|
element.id === issue.id ? { ...element, ...data } : element
|
||||||
issueId: issue.id,
|
);
|
||||||
},
|
_elements["sub_issues"] = [..._issues];
|
||||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
return _elements;
|
||||||
user as IUser
|
},
|
||||||
);
|
false
|
||||||
};
|
);
|
||||||
|
|
||||||
const partialUpdateIssue = async (data: Partial<IIssue>) => {
|
const issueResponse = await issueService.patchIssue(workspaceSlug as string, issue.project, issue.id, data, user);
|
||||||
mutate(
|
|
||||||
workspaceSlug && parentIssue ? SUB_ISSUES(parentIssue.id) : null,
|
|
||||||
(elements: any) => {
|
|
||||||
const _elements = { ...elements };
|
|
||||||
const _issues = _elements.sub_issues.map((element: IIssue) =>
|
|
||||||
element.id === issue.id ? { ...element, ...data } : element
|
|
||||||
);
|
|
||||||
_elements["sub_issues"] = [..._issues];
|
|
||||||
return _elements;
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const issueResponse = await issueService.patchIssue(workspaceSlug as string, issue.project, issue.id, data, user);
|
mutate(
|
||||||
|
SUB_ISSUES(parentIssue.id),
|
||||||
|
(elements: any) => {
|
||||||
|
const _elements = elements.sub_issues.map((element: IIssue) =>
|
||||||
|
element.id === issue.id ? issueResponse : element
|
||||||
|
);
|
||||||
|
elements["sub_issues"] = _elements;
|
||||||
|
return elements;
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
mutate(
|
return (
|
||||||
SUB_ISSUES(parentIssue.id),
|
<div className="relative flex items-center gap-1">
|
||||||
(elements: any) => {
|
{properties.priority && (
|
||||||
const _elements = elements.sub_issues.map((element: IIssue) =>
|
<div className="flex-shrink-0">
|
||||||
element.id === issue.id ? issueResponse : element
|
<PrioritySelect
|
||||||
);
|
value={issue.priority}
|
||||||
elements["sub_issues"] = _elements;
|
onChange={handlePriorityChange}
|
||||||
return elements;
|
hideDropdownArrow
|
||||||
},
|
disabled={!editable}
|
||||||
true
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center gap-1">
|
|
||||||
{properties.priority && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<PrioritySelect
|
|
||||||
value={issue.priority}
|
|
||||||
onChange={handlePriorityChange}
|
|
||||||
hideDropdownArrow
|
|
||||||
disabled={!editable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{properties.state && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<StateSelect
|
|
||||||
value={issue.state_detail}
|
|
||||||
projectId={issue.project_detail.id}
|
|
||||||
onChange={handleStateChange}
|
|
||||||
hideDropdownArrow
|
|
||||||
disabled={!editable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{properties.start_date && issue.start_date && (
|
|
||||||
<div className="flex-shrink-0 w-[104px]">
|
|
||||||
<ViewStartDateSelect
|
|
||||||
issue={issue}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
|
||||||
user={user}
|
|
||||||
isNotAllowed={!editable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{properties.due_date && issue.target_date && (
|
|
||||||
<div className="flex-shrink-0 w-[104px]">
|
|
||||||
{user && (
|
|
||||||
<ViewDueDateSelect
|
|
||||||
issue={issue}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
|
||||||
user={user}
|
|
||||||
isNotAllowed={!editable}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{properties.assignee && (
|
{properties.state && (
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<MembersSelect
|
<StateSelect
|
||||||
value={issue.assignees}
|
value={issue.state_detail}
|
||||||
projectId={issue.project_detail.id}
|
stateGroups={projectStore.states ? projectStore.states[issue.project] : undefined}
|
||||||
onChange={handleAssigneeChange}
|
onChange={(data) => handleStateChange(data)}
|
||||||
membersDetails={issue.assignee_details}
|
hideDropdownArrow
|
||||||
hideDropdownArrow
|
disabled={!editable}
|
||||||
disabled={!editable}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
{properties.start_date && issue.start_date && (
|
||||||
);
|
<div className="flex-shrink-0 w-[104px]">
|
||||||
};
|
<ViewStartDateSelect
|
||||||
|
issue={issue}
|
||||||
|
onChange={(val) => partialUpdateIssue({ start_date: val })}
|
||||||
|
disabled={!editable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{properties.due_date && issue.target_date && (
|
||||||
|
<div className="flex-shrink-0 w-[104px]">
|
||||||
|
{user && (
|
||||||
|
<ViewDueDateSelect
|
||||||
|
issue={issue}
|
||||||
|
onChange={(val) => partialUpdateIssue({ target_date: val })}
|
||||||
|
disabled={!editable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{properties.assignee && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<MembersSelect
|
||||||
|
value={issue.assignees}
|
||||||
|
onChange={(val) => handleAssigneeChange(val)}
|
||||||
|
members={projectStore.members ? (projectStore.members[issue.project] ?? []).map((m) => m.member) : []}
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={!editable}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -1,43 +1,30 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { findHowManyDaysLeft, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { findHowManyDaysLeft, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// services
|
|
||||||
import { TrackEventService } from "services/track_event.service";
|
|
||||||
// types
|
// types
|
||||||
import { IUser, IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
onChange: (date: string | null) => void;
|
||||||
handleOnOpen?: () => void;
|
handleOnOpen?: () => void;
|
||||||
handleOnClose?: () => void;
|
handleOnClose?: () => void;
|
||||||
tooltipPosition?: "top" | "bottom";
|
tooltipPosition?: "top" | "bottom";
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
user: IUser;
|
disabled: boolean;
|
||||||
isNotAllowed: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackEventService = new TrackEventService();
|
|
||||||
|
|
||||||
export const ViewDueDateSelect: React.FC<Props> = ({
|
export const ViewDueDateSelect: React.FC<Props> = ({
|
||||||
issue,
|
issue,
|
||||||
partialUpdateIssue,
|
onChange,
|
||||||
handleOnOpen,
|
handleOnOpen,
|
||||||
handleOnClose,
|
handleOnClose,
|
||||||
tooltipPosition = "top",
|
tooltipPosition = "top",
|
||||||
noBorder = false,
|
noBorder = false,
|
||||||
user,
|
disabled,
|
||||||
isNotAllowed,
|
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { displayFilters } = useIssuesView();
|
|
||||||
|
|
||||||
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
||||||
minDate?.setDate(minDate.getDate());
|
minDate?.setDate(minDate.getDate());
|
||||||
|
|
||||||
@ -59,34 +46,13 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
|||||||
<CustomDatePicker
|
<CustomDatePicker
|
||||||
placeholder="Due date"
|
placeholder="Due date"
|
||||||
value={issue?.target_date}
|
value={issue?.target_date}
|
||||||
onChange={(val) => {
|
onChange={onChange}
|
||||||
partialUpdateIssue(
|
className={`bg-transparent ${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}`}
|
||||||
{
|
|
||||||
target_date: val,
|
|
||||||
},
|
|
||||||
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_DUE_DATE",
|
|
||||||
user
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className={`${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"} ${
|
|
||||||
displayFilters.layout === "kanban" ? "bg-custom-background-90" : "bg-custom-background-100"
|
|
||||||
}`}
|
|
||||||
minDate={minDate ?? undefined}
|
minDate={minDate ?? undefined}
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
handleOnOpen={handleOnOpen}
|
handleOnOpen={handleOnOpen}
|
||||||
handleOnClose={handleOnClose}
|
handleOnClose={handleOnClose}
|
||||||
disabled={isNotAllowed}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
// services
|
|
||||||
import { TrackEventService } from "services/track_event.service";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useEstimateOption from "hooks/use-estimate-option";
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
// ui
|
// ui
|
||||||
@ -10,34 +7,23 @@ import { Tooltip } from "@plane/ui";
|
|||||||
// icons
|
// icons
|
||||||
import { Triangle } from "lucide-react";
|
import { Triangle } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IUser, IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
onChange: (data: number) => void;
|
||||||
position?: "left" | "right";
|
|
||||||
tooltipPosition?: "top" | "bottom";
|
tooltipPosition?: "top" | "bottom";
|
||||||
selfPositioned?: boolean;
|
|
||||||
customButton?: boolean;
|
customButton?: boolean;
|
||||||
user: IUser | undefined;
|
disabled: boolean;
|
||||||
isNotAllowed: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackEventService = new TrackEventService();
|
|
||||||
|
|
||||||
export const ViewEstimateSelect: React.FC<Props> = ({
|
export const ViewEstimateSelect: React.FC<Props> = ({
|
||||||
issue,
|
issue,
|
||||||
partialUpdateIssue,
|
onChange,
|
||||||
// position = "left",
|
|
||||||
tooltipPosition = "top",
|
tooltipPosition = "top",
|
||||||
// selfPositioned = false,
|
|
||||||
customButton = false,
|
customButton = false,
|
||||||
user,
|
disabled,
|
||||||
isNotAllowed,
|
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { isEstimateActive, estimatePoints } = useEstimateOption(issue.estimate_point);
|
const { isEstimateActive, estimatePoints } = useEstimateOption(issue.estimate_point);
|
||||||
|
|
||||||
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
|
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
|
||||||
@ -45,7 +31,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
const estimateLabels = (
|
const estimateLabels = (
|
||||||
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>
|
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>
|
||||||
<div className="flex items-center gap-1 text-custom-text-200">
|
<div className="flex items-center gap-1 text-custom-text-200">
|
||||||
<Triangle className="h-3.5 w-3.5" />
|
<Triangle className="h-3 w-3" />
|
||||||
{estimateValue ?? "None"}
|
{estimateValue ?? "None"}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -56,31 +42,17 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={issue.estimate_point}
|
value={issue.estimate_point}
|
||||||
onChange={(val: number) => {
|
onChange={onChange}
|
||||||
partialUpdateIssue({ estimate_point: val }, 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_ESTIMATE",
|
|
||||||
user as IUser
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
{...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })}
|
{...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })}
|
||||||
maxHeight="md"
|
maxHeight="md"
|
||||||
noChevron
|
noChevron
|
||||||
disabled={isNotAllowed}
|
disabled={disabled}
|
||||||
width="w-full min-w-[8rem]"
|
width="w-full min-w-[8rem]"
|
||||||
>
|
>
|
||||||
<CustomSelect.Option value={null}>
|
<CustomSelect.Option value={null}>
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
<Triangle className="h-4 w-4" />
|
<Triangle className="h-3 w-3" />
|
||||||
</span>
|
</span>
|
||||||
None
|
None
|
||||||
</>
|
</>
|
||||||
@ -88,9 +60,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
|||||||
{estimatePoints?.map((estimate) => (
|
{estimatePoints?.map((estimate) => (
|
||||||
<CustomSelect.Option key={estimate.id} value={estimate.key}>
|
<CustomSelect.Option key={estimate.id} value={estimate.key}>
|
||||||
<>
|
<>
|
||||||
<span>
|
<Triangle className="h-3 w-3" />
|
||||||
<Triangle className="h-4 w-4" />
|
|
||||||
</span>
|
|
||||||
{estimate.value}
|
{estimate.value}
|
||||||
</>
|
</>
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
|
@ -1,44 +1,30 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// services
|
|
||||||
import { TrackEventService } from "services/track_event.service";
|
|
||||||
// types
|
// types
|
||||||
import { IUser, IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
onChange: (date: string | null) => void;
|
||||||
handleOnOpen?: () => void;
|
handleOnOpen?: () => void;
|
||||||
handleOnClose?: () => void;
|
handleOnClose?: () => void;
|
||||||
tooltipPosition?: "top" | "bottom";
|
tooltipPosition?: "top" | "bottom";
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
user: IUser | undefined;
|
disabled: boolean;
|
||||||
isNotAllowed: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackEventService = new TrackEventService();
|
export const ViewStartDateSelect: React.FC<Props> = ({
|
||||||
|
|
||||||
export const ViewStartDateSelect: FC<Props> = ({
|
|
||||||
issue,
|
issue,
|
||||||
partialUpdateIssue,
|
onChange,
|
||||||
handleOnOpen,
|
handleOnOpen,
|
||||||
handleOnClose,
|
handleOnClose,
|
||||||
tooltipPosition = "top",
|
tooltipPosition = "top",
|
||||||
noBorder = false,
|
noBorder = false,
|
||||||
user,
|
disabled,
|
||||||
isNotAllowed,
|
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { displayFilters } = useIssuesView();
|
|
||||||
|
|
||||||
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
||||||
maxDate?.setDate(maxDate.getDate());
|
maxDate?.setDate(maxDate.getDate());
|
||||||
|
|
||||||
@ -52,34 +38,13 @@ export const ViewStartDateSelect: FC<Props> = ({
|
|||||||
<CustomDatePicker
|
<CustomDatePicker
|
||||||
placeholder="Start date"
|
placeholder="Start date"
|
||||||
value={issue?.start_date}
|
value={issue?.start_date}
|
||||||
onChange={(val) => {
|
onChange={onChange}
|
||||||
partialUpdateIssue(
|
className={`bg-transparent ${issue?.start_date ? "w-[6.5rem]" : "w-[5rem] text-center"}`}
|
||||||
{
|
|
||||||
start_date: val,
|
|
||||||
},
|
|
||||||
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_DUE_DATE",
|
|
||||||
user as IUser
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className={`${issue?.start_date ? "w-[6.5rem]" : "w-[5rem] text-center"} ${
|
|
||||||
displayFilters.layout === "kanban" ? "bg-custom-background-90" : "bg-custom-background-100"
|
|
||||||
}`}
|
|
||||||
maxDate={maxDate ?? undefined}
|
maxDate={maxDate ?? undefined}
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
handleOnOpen={handleOnOpen}
|
handleOnOpen={handleOnOpen}
|
||||||
handleOnClose={handleOnClose}
|
handleOnClose={handleOnClose}
|
||||||
disabled={isNotAllowed}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,31 +1,20 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
// react-popper
|
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
// services
|
|
||||||
import { IssueLabelService } from "services/issue";
|
|
||||||
// headless ui
|
|
||||||
import { Combobox } from "@headlessui/react";
|
|
||||||
// component
|
|
||||||
import { CreateLabelModal } from "components/labels";
|
|
||||||
// icons
|
|
||||||
import { Check, ChevronDown, PlusIcon, Search } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { Tooltip } from "components/ui";
|
|
||||||
import { IUser, IIssueLabels } from "types";
|
|
||||||
import { Placement } from "@popperjs/core";
|
import { Placement } from "@popperjs/core";
|
||||||
// constants
|
import { Combobox } from "@headlessui/react";
|
||||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
import { Check, ChevronDown, PlusIcon, Search } from "lucide-react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { CreateLabelModal } from "components/labels";
|
||||||
|
// ui
|
||||||
|
import { Tooltip } from "components/ui";
|
||||||
|
// types
|
||||||
|
import { IIssueLabels } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string[];
|
value: string[];
|
||||||
projectId: string;
|
onChange: (data: string[]) => void;
|
||||||
onChange: (data: any) => void;
|
labels: IIssueLabels[];
|
||||||
labelsDetails: any[];
|
|
||||||
className?: string;
|
className?: string;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
@ -33,17 +22,12 @@ type Props = {
|
|||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
hideDropdownArrow?: boolean;
|
hideDropdownArrow?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
user: IUser | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// services
|
|
||||||
const issueLabelService = new IssueLabelService();
|
|
||||||
|
|
||||||
export const LabelSelect: React.FC<Props> = ({
|
export const LabelSelect: React.FC<Props> = ({
|
||||||
value,
|
value,
|
||||||
projectId,
|
|
||||||
onChange,
|
onChange,
|
||||||
labelsDetails,
|
labels,
|
||||||
className = "",
|
className = "",
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
optionsClassName = "",
|
optionsClassName = "",
|
||||||
@ -51,31 +35,19 @@ export const LabelSelect: React.FC<Props> = ({
|
|||||||
placement,
|
placement,
|
||||||
hideDropdownArrow = false,
|
hideDropdownArrow = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
user,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [fetchStates, setFetchStates] = useState(false);
|
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const [labelModal, setLabelModal] = useState(false);
|
const [labelModal, setLabelModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: placement ?? "bottom-start",
|
placement: placement ?? "bottom-start",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
const options = labels?.map((label) => ({
|
||||||
projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId) : null,
|
|
||||||
workspaceSlug && projectId && fetchStates
|
|
||||||
? () => issueLabelService.getProjectIssueLabels(workspaceSlug.toString(), projectId)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const options = issueLabels?.map((label) => ({
|
|
||||||
value: label.id,
|
value: label.id,
|
||||||
query: label.name,
|
query: label.name,
|
||||||
content: (
|
content: (
|
||||||
@ -94,48 +66,6 @@ export const LabelSelect: React.FC<Props> = ({
|
|||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
const label = (
|
|
||||||
<div className={`flex items-center gap-2 text-custom-text-200`}>
|
|
||||||
{labelsDetails.length > 0 ? (
|
|
||||||
labelsDetails.length <= maxRender ? (
|
|
||||||
<>
|
|
||||||
{labelsDetails.map((label) => (
|
|
||||||
<div
|
|
||||||
key={label.id}
|
|
||||||
className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
|
||||||
<span
|
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: label?.color ?? "#000000",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{label.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
|
||||||
<Tooltip
|
|
||||||
position="top"
|
|
||||||
tooltipHeading="Labels"
|
|
||||||
tooltipContent={labelsDetails.map((l) => l.name).join(", ")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
|
||||||
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
|
||||||
{`${value.length} Labels`}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const footerOption = (
|
const footerOption = (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -151,14 +81,15 @@ export const LabelSelect: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{projectId && (
|
{/* TODO: update this logic */}
|
||||||
|
{/* {projectId && (
|
||||||
<CreateLabelModal
|
<CreateLabelModal
|
||||||
isOpen={labelModal}
|
isOpen={labelModal}
|
||||||
handleClose={() => setLabelModal(false)}
|
handleClose={() => setLabelModal(false)}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
<Combobox
|
<Combobox
|
||||||
as="div"
|
as="div"
|
||||||
className={`flex-shrink-0 text-left ${className}`}
|
className={`flex-shrink-0 text-left ${className}`}
|
||||||
@ -167,81 +98,116 @@ export const LabelSelect: React.FC<Props> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
multiple
|
multiple
|
||||||
>
|
>
|
||||||
{({ open }: { open: boolean }) => {
|
<Combobox.Button as={React.Fragment}>
|
||||||
if (open) setFetchStates(true);
|
<button
|
||||||
|
ref={setReferenceElement}
|
||||||
return (
|
type="button"
|
||||||
<>
|
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||||
<Combobox.Button as={React.Fragment}>
|
disabled
|
||||||
<button
|
? "cursor-not-allowed text-custom-text-200"
|
||||||
ref={setReferenceElement}
|
: value.length <= maxRender
|
||||||
type="button"
|
? "cursor-pointer"
|
||||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
: "cursor-pointer hover:bg-custom-background-80"
|
||||||
disabled
|
} ${buttonClassName}`}
|
||||||
? "cursor-not-allowed text-custom-text-200"
|
>
|
||||||
: value.length <= maxRender
|
<div className={`flex items-center gap-2 text-custom-text-200`}>
|
||||||
? "cursor-pointer"
|
{value.length > 0 ? (
|
||||||
: "cursor-pointer hover:bg-custom-background-80"
|
value.length <= maxRender ? (
|
||||||
} ${buttonClassName}`}
|
<>
|
||||||
>
|
{labels
|
||||||
{label}
|
.filter((l) => value.includes(l.id))
|
||||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
.map((label) => (
|
||||||
</button>
|
<div
|
||||||
</Combobox.Button>
|
key={label.id}
|
||||||
|
className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||||
<Combobox.Options>
|
>
|
||||||
<div
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
<span
|
||||||
ref={setPopperElement}
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
style={styles.popper}
|
style={{
|
||||||
{...attributes.popper}
|
backgroundColor: label?.color ?? "#000000",
|
||||||
>
|
}}
|
||||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
/>
|
||||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
{label.name}
|
||||||
<Combobox.Input
|
</div>
|
||||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
</div>
|
||||||
value={query}
|
))}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
</>
|
||||||
placeholder="Search"
|
) : (
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
<div className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
||||||
/>
|
<Tooltip
|
||||||
|
position="top"
|
||||||
|
tooltipHeading="Labels"
|
||||||
|
tooltipContent={labels
|
||||||
|
.filter((l) => value.includes(l.id))
|
||||||
|
.map((l) => l.name)
|
||||||
|
.join(", ")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
|
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
||||||
|
{`${value.length} Labels`}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
)
|
||||||
{filteredOptions ? (
|
) : (
|
||||||
filteredOptions.length > 0 ? (
|
""
|
||||||
filteredOptions.map((option) => (
|
)}
|
||||||
<Combobox.Option
|
</div>
|
||||||
key={option.value}
|
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||||
value={option.value}
|
</button>
|
||||||
className={({ active, selected }) =>
|
</Combobox.Button>
|
||||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
|
||||||
active && !selected ? "bg-custom-background-80" : ""
|
<Combobox.Options>
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
<div
|
||||||
}
|
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||||
>
|
ref={setPopperElement}
|
||||||
{({ selected }) => (
|
style={styles.popper}
|
||||||
<>
|
{...attributes.popper}
|
||||||
{option.content}
|
>
|
||||||
{selected && <Check className="h-3.5 w-3.5" />}
|
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||||
</>
|
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
)}
|
<Combobox.Input
|
||||||
</Combobox.Option>
|
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
))
|
value={query}
|
||||||
) : (
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
<span className="flex items-center gap-2 p-1">
|
placeholder="Search"
|
||||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
</span>
|
/>
|
||||||
)
|
</div>
|
||||||
) : (
|
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||||
<p className="text-center text-custom-text-200">Loading...</p>
|
{filteredOptions ? (
|
||||||
)}
|
filteredOptions.length > 0 ? (
|
||||||
</div>
|
filteredOptions.map((option) => (
|
||||||
{footerOption}
|
<Combobox.Option
|
||||||
</div>
|
key={option.value}
|
||||||
</Combobox.Options>
|
value={option.value}
|
||||||
</>
|
className={({ active, selected }) =>
|
||||||
);
|
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||||
}}
|
active && !selected ? "bg-custom-background-80" : ""
|
||||||
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
{option.content}
|
||||||
|
{selected && <Check className={`h-3.5 w-3.5`} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-2 p-1">
|
||||||
|
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-custom-text-200">Loading...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{footerOption}
|
||||||
|
</div>
|
||||||
|
</Combobox.Options>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
// react-popper
|
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
// hooks
|
import { Placement } from "@popperjs/core";
|
||||||
import useProjectMembers from "hooks/use-project-members";
|
|
||||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
// components
|
// components
|
||||||
@ -14,65 +9,57 @@ import { AssigneesList, Avatar, Tooltip } from "components/ui";
|
|||||||
// icons
|
// icons
|
||||||
import { Check, ChevronDown, Search, User2 } from "lucide-react";
|
import { Check, ChevronDown, Search, User2 } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IUser } from "types";
|
import { IUserLite } from "types";
|
||||||
import { Placement } from "@popperjs/core";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string | string[];
|
members: IUserLite[];
|
||||||
projectId: string;
|
|
||||||
onChange: (data: any) => void;
|
|
||||||
membersDetails: IUser[];
|
|
||||||
renderWorkspaceMembers?: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
hideDropdownArrow?: boolean;
|
hideDropdownArrow?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
} & (
|
||||||
|
| {
|
||||||
|
value: string[];
|
||||||
|
onChange: (data: string[]) => void;
|
||||||
|
multiple: true;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: string;
|
||||||
|
onChange: (data: string) => void;
|
||||||
|
multiple: false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const MembersSelect: React.FC<Props> = ({
|
export const MembersSelect: React.FC<Props> = ({
|
||||||
value,
|
value,
|
||||||
projectId,
|
|
||||||
onChange,
|
onChange,
|
||||||
membersDetails,
|
members,
|
||||||
renderWorkspaceMembers = false,
|
|
||||||
className = "",
|
className = "",
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
optionsClassName = "",
|
optionsClassName = "",
|
||||||
placement,
|
placement,
|
||||||
hideDropdownArrow = false,
|
hideDropdownArrow = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
multiple = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [fetchStates, setFetchStates] = useState(false);
|
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: placement ?? "bottom-start",
|
placement: placement ?? "bottom-start",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId, fetchStates && !renderWorkspaceMembers);
|
const options = members?.map((member) => ({
|
||||||
|
value: member.id,
|
||||||
const { workspaceMembers } = useWorkspaceMembers(
|
query: member.display_name,
|
||||||
workspaceSlug?.toString() ?? "",
|
|
||||||
fetchStates && renderWorkspaceMembers
|
|
||||||
);
|
|
||||||
|
|
||||||
const membersOptions = renderWorkspaceMembers ? workspaceMembers : members;
|
|
||||||
|
|
||||||
const options = membersOptions?.map((member) => ({
|
|
||||||
value: member.member.id,
|
|
||||||
query: member.member.display_name,
|
|
||||||
content: (
|
content: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar user={member.member} />
|
<Avatar user={member} />
|
||||||
{member.member.display_name}
|
{member.display_name}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
@ -84,8 +71,11 @@ export const MembersSelect: React.FC<Props> = ({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipHeading="Assignee"
|
tooltipHeading="Assignee"
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
membersDetails && membersDetails.length > 0
|
value && value.length > 0
|
||||||
? membersDetails.map((assignee) => assignee?.display_name).join(", ")
|
? members
|
||||||
|
.filter((m) => value.includes(m.display_name))
|
||||||
|
.map((m) => m.display_name)
|
||||||
|
.join(", ")
|
||||||
: "No Assignee"
|
: "No Assignee"
|
||||||
}
|
}
|
||||||
position="top"
|
position="top"
|
||||||
@ -105,84 +95,72 @@ export const MembersSelect: React.FC<Props> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const comboboxProps: any = { value, onChange, disabled };
|
||||||
<Combobox
|
if (multiple) comboboxProps.multiple = true;
|
||||||
as="div"
|
|
||||||
className={`flex-shrink-0 text-left ${className}`}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled={disabled}
|
|
||||||
multiple
|
|
||||||
>
|
|
||||||
{({ open }: { open: boolean }) => {
|
|
||||||
if (open) setFetchStates(true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Combobox as="div" className={`flex-shrink-0 text-left ${className}`} {...comboboxProps}>
|
||||||
<Combobox.Button as={React.Fragment}>
|
<Combobox.Button as={React.Fragment}>
|
||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||||
</button>
|
</button>
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
<Combobox.Options>
|
<Combobox.Options>
|
||||||
<div
|
<div
|
||||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||||
ref={setPopperElement}
|
ref={setPopperElement}
|
||||||
style={styles.popper}
|
style={styles.popper}
|
||||||
{...attributes.popper}
|
{...attributes.popper}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||||
{filteredOptions ? (
|
{filteredOptions ? (
|
||||||
filteredOptions.length > 0 ? (
|
filteredOptions.length > 0 ? (
|
||||||
filteredOptions.map((option) => (
|
filteredOptions.map((option) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||||
active && !selected ? "bg-custom-background-80" : ""
|
active && !selected ? "bg-custom-background-80" : ""
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
{option.content}
|
{option.content}
|
||||||
{selected && <Check className="h-3.5 w-3.5" />}
|
{selected && <Check className={`h-3.5 w-3.5`} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-2 p-1">
|
<span className="flex items-center gap-2 p-1">
|
||||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-custom-text-200">Loading...</p>
|
<p className="text-center text-custom-text-200">Loading...</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Combobox.Options>
|
</Combobox.Options>
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Combobox>
|
</Combobox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,32 +1,21 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// react-popper
|
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
// services
|
|
||||||
import { ProjectStateService } from "services/project";
|
|
||||||
// headless ui
|
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
// icons
|
|
||||||
import { Check, ChevronDown, Search } from "lucide-react";
|
import { Check, ChevronDown, Search } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { getStatesList } from "helpers/state.helper";
|
||||||
// types
|
// types
|
||||||
import { Tooltip } from "components/ui";
|
import { Tooltip } from "components/ui";
|
||||||
import { Placement } from "@popperjs/core";
|
import { Placement } from "@popperjs/core";
|
||||||
// constants
|
// constants
|
||||||
import { IState } from "types";
|
import { IState, IStateResponse } from "types";
|
||||||
import { STATES_LIST } from "constants/fetch-keys";
|
|
||||||
// helper
|
|
||||||
import { getStatesList } from "helpers/state.helper";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: IState;
|
value: IState;
|
||||||
onChange: (data: any, states: IState[] | undefined) => void;
|
onChange: (state: IState) => void;
|
||||||
projectId: string;
|
stateGroups: IStateResponse | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
@ -35,13 +24,10 @@ type Props = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// services
|
|
||||||
const projectStateService = new ProjectStateService();
|
|
||||||
|
|
||||||
export const StateSelect: React.FC<Props> = ({
|
export const StateSelect: React.FC<Props> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
projectId,
|
stateGroups,
|
||||||
className = "",
|
className = "",
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
optionsClassName = "",
|
optionsClassName = "",
|
||||||
@ -54,22 +40,10 @@ export const StateSelect: React.FC<Props> = ({
|
|||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const [fetchStates, setFetchStates] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: placement ?? "bottom-start",
|
placement: placement ?? "bottom-start",
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
|
||||||
workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId) : null,
|
|
||||||
workspaceSlug && projectId && fetchStates
|
|
||||||
? () => projectStateService.getStates(workspaceSlug.toString(), projectId)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const states = getStatesList(stateGroups);
|
const states = getStatesList(stateGroups);
|
||||||
|
|
||||||
const options = states?.map((state) => ({
|
const options = states?.map((state) => ({
|
||||||
@ -101,79 +75,73 @@ export const StateSelect: React.FC<Props> = ({
|
|||||||
className={`flex-shrink-0 text-left ${className}`}
|
className={`flex-shrink-0 text-left ${className}`}
|
||||||
value={value.id}
|
value={value.id}
|
||||||
onChange={(data: string) => {
|
onChange={(data: string) => {
|
||||||
onChange(data, states);
|
const selectedState = states?.find((state) => state.id === data);
|
||||||
|
|
||||||
|
if (selectedState) onChange(selectedState);
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{({ open }: { open: boolean }) => {
|
<Combobox.Button as={React.Fragment}>
|
||||||
if (open) setFetchStates(true);
|
<button
|
||||||
|
ref={setReferenceElement}
|
||||||
return (
|
type="button"
|
||||||
<>
|
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||||
<Combobox.Button as={React.Fragment}>
|
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||||
<button
|
} ${buttonClassName}`}
|
||||||
ref={setReferenceElement}
|
>
|
||||||
type="button"
|
{label}
|
||||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
</button>
|
||||||
} ${buttonClassName}`}
|
</Combobox.Button>
|
||||||
>
|
<Combobox.Options>
|
||||||
{label}
|
<div
|
||||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||||
</button>
|
ref={setPopperElement}
|
||||||
</Combobox.Button>
|
style={styles.popper}
|
||||||
<Combobox.Options>
|
{...attributes.popper}
|
||||||
<div
|
>
|
||||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||||
ref={setPopperElement}
|
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
style={styles.popper}
|
<Combobox.Input
|
||||||
{...attributes.popper}
|
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
>
|
value={query}
|
||||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
placeholder="Search"
|
||||||
<Combobox.Input
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
/>
|
||||||
value={query}
|
</div>
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||||
placeholder="Search"
|
{filteredOptions ? (
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
filteredOptions.length > 0 ? (
|
||||||
/>
|
filteredOptions.map((option) => (
|
||||||
</div>
|
<Combobox.Option
|
||||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
key={option.value}
|
||||||
{filteredOptions ? (
|
value={option.value}
|
||||||
filteredOptions.length > 0 ? (
|
className={({ active, selected }) =>
|
||||||
filteredOptions.map((option) => (
|
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||||
<Combobox.Option
|
active && !selected ? "bg-custom-background-80" : ""
|
||||||
key={option.value}
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
value={option.value}
|
}
|
||||||
className={({ active, selected }) =>
|
>
|
||||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
{({ selected }) => (
|
||||||
active && !selected ? "bg-custom-background-80" : ""
|
<>
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
{option.content}
|
||||||
}
|
{selected && <Check className="h-3.5 w-3.5" />}
|
||||||
>
|
</>
|
||||||
{({ selected }) => (
|
)}
|
||||||
<>
|
</Combobox.Option>
|
||||||
{option.content}
|
))
|
||||||
{selected && <Check className="h-3.5 w-3.5" />}
|
) : (
|
||||||
</>
|
<span className="flex items-center gap-2 p-1">
|
||||||
)}
|
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||||
</Combobox.Option>
|
</span>
|
||||||
))
|
)
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-2 p-1">
|
<p className="text-center text-custom-text-200">Loading...</p>
|
||||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
)}
|
||||||
</span>
|
</div>
|
||||||
)
|
</div>
|
||||||
) : (
|
</Combobox.Options>
|
||||||
<p className="text-center text-custom-text-200">Loading...</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Combobox.Options>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Combobox>
|
</Combobox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,80 +1,75 @@
|
|||||||
import { CalendarDaysIcon, PlayIcon, Squares2X2Icon, TagIcon, UserGroupIcon } from "@heroicons/react/24/outline";
|
import { TIssueOrderByOptions } from "types";
|
||||||
|
|
||||||
export const SPREADSHEET_COLUMN = [
|
export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||||
{
|
[key: string]: {
|
||||||
propertyName: "title",
|
title: string;
|
||||||
colName: "Title",
|
ascendingOrderKey: TIssueOrderByOptions;
|
||||||
colSize: "440px",
|
ascendingOrderTitle: string;
|
||||||
|
descendingOrderKey: TIssueOrderByOptions;
|
||||||
|
descendingOrderTitle: string;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
assignee: {
|
||||||
|
title: "Assignees",
|
||||||
|
ascendingOrderKey: "assignees__first_name",
|
||||||
|
ascendingOrderTitle: "A",
|
||||||
|
descendingOrderKey: "-assignees__first_name",
|
||||||
|
descendingOrderTitle: "Z",
|
||||||
},
|
},
|
||||||
{
|
created_on: {
|
||||||
propertyName: "state",
|
title: "Created on",
|
||||||
colName: "State",
|
ascendingOrderKey: "-created_at",
|
||||||
colSize: "128px",
|
ascendingOrderTitle: "New",
|
||||||
icon: Squares2X2Icon,
|
descendingOrderKey: "created_at",
|
||||||
ascendingOrder: "state__name",
|
descendingOrderTitle: "Old",
|
||||||
descendingOrder: "-state__name",
|
|
||||||
},
|
},
|
||||||
{
|
due_date: {
|
||||||
propertyName: "priority",
|
title: "Due date",
|
||||||
colName: "Priority",
|
ascendingOrderKey: "-target_date",
|
||||||
colSize: "128px",
|
ascendingOrderTitle: "New",
|
||||||
ascendingOrder: "priority",
|
descendingOrderKey: "target_date",
|
||||||
descendingOrder: "-priority",
|
descendingOrderTitle: "Old",
|
||||||
},
|
},
|
||||||
{
|
estimate: {
|
||||||
propertyName: "assignee",
|
title: "Estimate",
|
||||||
colName: "Assignees",
|
ascendingOrderKey: "estimate_point",
|
||||||
colSize: "128px",
|
ascendingOrderTitle: "Low",
|
||||||
icon: UserGroupIcon,
|
descendingOrderKey: "-estimate_point",
|
||||||
ascendingOrder: "assignees__id",
|
descendingOrderTitle: "High",
|
||||||
descendingOrder: "-assignees__id",
|
|
||||||
},
|
},
|
||||||
{
|
labels: {
|
||||||
propertyName: "labels",
|
title: "Labels",
|
||||||
colName: "Labels",
|
ascendingOrderKey: "labels__name",
|
||||||
colSize: "128px",
|
ascendingOrderTitle: "A",
|
||||||
icon: TagIcon,
|
descendingOrderKey: "-labels__name",
|
||||||
ascendingOrder: "labels__name",
|
descendingOrderTitle: "Z",
|
||||||
descendingOrder: "-labels__name",
|
|
||||||
},
|
},
|
||||||
{
|
priority: {
|
||||||
propertyName: "start_date",
|
title: "Priority",
|
||||||
colName: "Start Date",
|
ascendingOrderKey: "priority",
|
||||||
colSize: "128px",
|
ascendingOrderTitle: "None",
|
||||||
icon: CalendarDaysIcon,
|
descendingOrderKey: "-priority",
|
||||||
ascendingOrder: "-start_date",
|
descendingOrderTitle: "Urgent",
|
||||||
descendingOrder: "start_date",
|
|
||||||
},
|
},
|
||||||
{
|
start_date: {
|
||||||
propertyName: "due_date",
|
title: "Start date",
|
||||||
colName: "Due Date",
|
ascendingOrderKey: "-start_date",
|
||||||
colSize: "128px",
|
ascendingOrderTitle: "New",
|
||||||
icon: CalendarDaysIcon,
|
descendingOrderKey: "start_date",
|
||||||
ascendingOrder: "-target_date",
|
descendingOrderTitle: "Old",
|
||||||
descendingOrder: "target_date",
|
|
||||||
},
|
},
|
||||||
{
|
state: {
|
||||||
propertyName: "estimate",
|
title: "State",
|
||||||
colName: "Estimate",
|
ascendingOrderKey: "state__name",
|
||||||
colSize: "128px",
|
ascendingOrderTitle: "A",
|
||||||
icon: PlayIcon,
|
descendingOrderKey: "-state__name",
|
||||||
ascendingOrder: "estimate_point",
|
descendingOrderTitle: "Z",
|
||||||
descendingOrder: "-estimate_point",
|
|
||||||
},
|
},
|
||||||
{
|
updated_on: {
|
||||||
propertyName: "created_on",
|
title: "Updated on",
|
||||||
colName: "Created On",
|
ascendingOrderKey: "-updated_at",
|
||||||
colSize: "144px",
|
ascendingOrderTitle: "New",
|
||||||
icon: CalendarDaysIcon,
|
descendingOrderKey: "updated_at",
|
||||||
ascendingOrder: "-created_at",
|
descendingOrderTitle: "Old",
|
||||||
descendingOrder: "created_at",
|
|
||||||
},
|
},
|
||||||
{
|
};
|
||||||
propertyName: "updated_on",
|
|
||||||
colName: "Updated On",
|
|
||||||
colSize: "144px",
|
|
||||||
icon: CalendarDaysIcon,
|
|
||||||
ascendingOrder: "-updated_at",
|
|
||||||
descendingOrder: "updated_at",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
@ -21,13 +21,13 @@ export interface IIssueDetailStore {
|
|||||||
setPeekId: (issueId: string | null) => void;
|
setPeekId: (issueId: string | null) => void;
|
||||||
setPeekMode: (issueId: IPeekMode | null) => void;
|
setPeekMode: (issueId: IPeekMode | null) => void;
|
||||||
// fetch issue details
|
// fetch issue details
|
||||||
fetchIssueDetails: (workspaceId: string, projectId: string, issueId: string) => void;
|
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => void;
|
||||||
// creating issue
|
// creating issue
|
||||||
createIssue: (workspaceId: string, projectId: string, data: Partial<IIssue>, user: IUser) => void;
|
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => void;
|
||||||
// updating issue
|
// updating issue
|
||||||
updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>, user: IUser) => void;
|
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>, user: IUser) => void;
|
||||||
// deleting issue
|
// deleting issue
|
||||||
deleteIssue: (workspaceId: string, projectId: string, issueId: string, user: IUser) => void;
|
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IssueDetailStore implements IIssueDetailStore {
|
export class IssueDetailStore implements IIssueDetailStore {
|
||||||
@ -74,12 +74,12 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
|
|
||||||
setPeekMode = (mode: IPeekMode | null) => (this.peekMode = mode);
|
setPeekMode = (mode: IPeekMode | null) => (this.peekMode = mode);
|
||||||
|
|
||||||
fetchIssueDetails = async (workspaceId: string, projectId: string, issueId: string) => {
|
fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const issueDetailsResponse = await this.issueService.retrieve(workspaceId, projectId, issueId);
|
const issueDetailsResponse = await this.issueService.retrieve(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
@ -99,14 +99,14 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createIssue = async (workspaceId: string, projectId: string, data: Partial<IIssue>, user: IUser) => {
|
createIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await this.issueService.createIssues(workspaceId, projectId, data, user);
|
const response = await this.issueService.createIssues(workspaceSlug, projectId, data, user);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
@ -124,7 +124,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateIssue = async (
|
updateIssue = async (
|
||||||
workspaceId: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
issueId: string,
|
issueId: string,
|
||||||
data: Partial<IIssue>,
|
data: Partial<IIssue>,
|
||||||
@ -143,7 +143,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
this.issues = newIssues;
|
this.issues = newIssues;
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await this.issueService.patchIssue(workspaceId, projectId, issueId, data, user);
|
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data, user);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
@ -157,7 +157,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fetchIssueDetails(workspaceId, projectId, issueId);
|
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
@ -168,7 +168,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteIssue = async (workspaceId: string, projectId: string, issueId: string, user: IUser) => {
|
deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => {
|
||||||
const newIssues = { ...this.issues };
|
const newIssues = { ...this.issues };
|
||||||
delete newIssues[issueId];
|
delete newIssues[issueId];
|
||||||
|
|
||||||
@ -179,14 +179,14 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
this.issues = newIssues;
|
this.issues = newIssues;
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.issueService.deleteIssue(workspaceId, projectId, issueId, user);
|
await this.issueService.deleteIssue(workspaceSlug, projectId, issueId, user);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fetchIssueDetails(workspaceId, projectId, issueId);
|
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
|
@ -5,12 +5,14 @@ import { IssueService } from "services/issue";
|
|||||||
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
|
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "../root";
|
import { RootStore } from "../root";
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssue, IIssueFilterOptions } from "types";
|
||||||
import {
|
import {
|
||||||
IIssueGroupWithSubGroupsStructure,
|
IIssueGroupWithSubGroupsStructure,
|
||||||
IIssueGroupedStructure,
|
IIssueGroupedStructure,
|
||||||
IIssueUnGroupedStructure,
|
IIssueUnGroupedStructure,
|
||||||
} from "../module/module_issue.store";
|
} from "../module/module_issue.store";
|
||||||
|
// helpers
|
||||||
|
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
|
||||||
|
|
||||||
export interface IProjectViewIssuesStore {
|
export interface IProjectViewIssuesStore {
|
||||||
// states
|
// states
|
||||||
@ -27,6 +29,7 @@ export interface IProjectViewIssuesStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
|
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
||||||
fetchViewIssues: (
|
fetchViewIssues: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -68,6 +71,7 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
|
|||||||
viewIssues: observable.ref,
|
viewIssues: observable.ref,
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
|
updateIssueStructure: action,
|
||||||
fetchViewIssues: action,
|
fetchViewIssues: action,
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
@ -99,6 +103,56 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
|
|||||||
return this.viewIssues?.[viewId]?.[issueType] || null;
|
return this.viewIssues?.[viewId]?.[issueType] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
||||||
|
const viewId: string | null = this.rootStore.projectViews.viewId;
|
||||||
|
const issueType = this.rootStore.issue.getIssueType;
|
||||||
|
if (!viewId || !issueType) return null;
|
||||||
|
|
||||||
|
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
|
||||||
|
this.getIssues;
|
||||||
|
if (!issues) return null;
|
||||||
|
|
||||||
|
if (issueType === "grouped" && group_id) {
|
||||||
|
issues = issues as IIssueGroupedStructure;
|
||||||
|
issues = {
|
||||||
|
...issues,
|
||||||
|
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
||||||
|
issues = issues as IIssueGroupWithSubGroupsStructure;
|
||||||
|
issues = {
|
||||||
|
...issues,
|
||||||
|
[sub_group_id]: {
|
||||||
|
...issues[sub_group_id],
|
||||||
|
[group_id]: issues[sub_group_id][group_id].map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (issueType === "ungrouped") {
|
||||||
|
issues = issues as IIssueUnGroupedStructure;
|
||||||
|
issues = issues.map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i));
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || "";
|
||||||
|
if (orderBy === "-created_at") {
|
||||||
|
issues = sortArrayByDate(issues as any, "created_at");
|
||||||
|
}
|
||||||
|
if (orderBy === "-updated_at") {
|
||||||
|
issues = sortArrayByDate(issues as any, "updated_at");
|
||||||
|
}
|
||||||
|
if (orderBy === "start_date") {
|
||||||
|
issues = sortArrayByDate(issues as any, "updated_at");
|
||||||
|
}
|
||||||
|
if (orderBy === "priority") {
|
||||||
|
issues = sortArrayByPriority(issues as any, "priority");
|
||||||
|
}
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.viewIssues = { ...this.viewIssues, [viewId]: { ...this.viewIssues[viewId], [issueType]: issues } };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
fetchViewIssues = async (workspaceSlug: string, projectId: string, viewId: string, filters: IIssueFilterOptions) => {
|
fetchViewIssues = async (workspaceSlug: string, projectId: string, viewId: string, filters: IIssueFilterOptions) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
@ -139,7 +139,6 @@ class UserStore implements IUserStore {
|
|||||||
try {
|
try {
|
||||||
const response = await this.projectService.projectMemberMe(workspaceSlug, projectId);
|
const response = await this.projectService.projectMemberMe(workspaceSlug, projectId);
|
||||||
|
|
||||||
console.log("response", response);
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.projectMemberInfo = response;
|
this.projectMemberInfo = response;
|
||||||
this.hasPermissionToWorkspace = true;
|
this.hasPermissionToWorkspace = true;
|
||||||
|
Loading…
Reference in New Issue
Block a user