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

* refactor: spreadsheet layout components

* refactor: spreadsheet properties

* refactor: folder structure

* chore: issue property update

* chore: spreadsheet layout in the global views

* style: quick actions menu

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -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>
</>
);
});

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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";

View File

@ -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>

View File

@ -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}
/> />

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
</>
);
});

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
</>
);
});

View File

@ -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>
</>
);
});

View File

@ -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}
/>
);
});

View File

@ -0,0 +1,4 @@
export * from "./cycle-root";
export * from "./module-root";
export * from "./project-root";
export * from "./project-view-root";

View File

@ -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}
/>
);
});

View File

@ -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}
/>
);
});

View File

@ -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}
/>
);
});

View File

@ -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>
);
};

View File

@ -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"
/>
)}
</>
);
});

View File

@ -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>
</>
);
});

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
}
);

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
</> </>
); );

View File

@ -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>
); );
}; };

View File

@ -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>
); );
}; };

View File

@ -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",
},
];

View File

@ -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;

View File

@ -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(() => {

View File

@ -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;