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

* refactor: spreadsheet layout components

* refactor: spreadsheet properties

* refactor: folder structure

* chore: issue property update

* chore: spreadsheet layout in the global views

* style: quick actions menu

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

View File

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

View File

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

View File

@ -1,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
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { SpreadsheetView } from "components/core";
import { GlobalViewsAppliedFiltersRoot } from "components/issues";
import { GlobalViewsAppliedFiltersRoot, SpreadsheetView } from "components/issues";
// types
import { IIssueDisplayFilterOptions, TStaticViewTypes } from "types";
import { IIssue, IIssueDisplayFilterOptions, TStaticViewTypes } from "types";
// fetch-keys
import { GLOBAL_VIEW_ISSUES } from "constants/fetch-keys";
@ -28,6 +27,7 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
globalViewIssues: globalViewIssuesStore,
globalViewFilters: globalViewFiltersStore,
workspaceFilter: workspaceFilterStore,
workspace: workspaceStore,
} = useMobxStore();
const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined;
@ -63,6 +63,18 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
[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
? globalViewIssuesStore.viewIssues?.[type]
: globalViewId
@ -78,8 +90,10 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
displayFilters={workspaceFilterStore.workspaceDisplayFilters}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues}
members={workspaceStore.workspaceMembers ? workspaceStore.workspaceMembers.map((m) => m.member) : undefined}
labels={workspaceStore.workspaceLabels ? workspaceStore.workspaceLabels : undefined}
handleIssueAction={() => {}}
handleUpdateIssue={() => {}}
handleUpdateIssue={handleUpdateIssue}
disableUserActions={false}
/>
</div>

View File

@ -12,32 +12,27 @@ import {
GanttLayout,
KanBanLayout,
ProjectAppliedFiltersRoot,
SpreadsheetLayout,
ProjectSpreadsheetLayout,
} from "components/issues";
export const ProjectLayoutRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query as {
workspaceSlug: string;
projectId: string;
cycleId: string;
moduleId: string;
};
const { workspaceSlug, projectId } = router.query;
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES` : null,
workspaceSlug && projectId ? `REVALIDATE_PROJECT_ISSUES_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
await projectStore.fetchProjectStates(workspaceSlug, projectId);
await projectStore.fetchProjectLabels(workspaceSlug, projectId);
await projectStore.fetchProjectMembers(workspaceSlug, projectId);
await projectStore.fetchProjectEstimates(workspaceSlug, projectId);
await projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString());
await projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString());
await projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString());
await projectStore.fetchProjectEstimates(workspaceSlug.toString(), projectId.toString());
await issueStore.fetchIssues(workspaceSlug, projectId);
await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
}
},
{ revalidateOnFocus: false }
@ -58,7 +53,7 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<SpreadsheetLayout />
<ProjectSpreadsheetLayout />
) : null}
</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 "./module-root";
export * from "./project-view-root";
export * from "./root";
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 "./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 { useRouter } from "next/router";
// components
import { Popover2 } from "@blueprintjs/popover2";
// icons
import { MoreHorizontal, LinkIcon, Pencil, Trash2, ChevronRight } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { IIssue, Properties } from "types";
// helper
import { copyTextToClipboard } from "helpers/string.helper";
type Props = {
issue: IIssue;
@ -21,7 +17,6 @@ type Props = {
properties: Properties;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
disableUserActions: boolean;
nestingLevel: number;
};
@ -34,7 +29,6 @@ export const IssueColumn: React.FC<Props> = ({
properties,
handleEditIssue,
handleDeleteIssue,
setCurrentProjectId,
disableUserActions,
nestingLevel,
}) => {
@ -48,7 +42,7 @@ export const IssueColumn: React.FC<Props> = ({
const openPeekOverview = () => {
const { query } = router;
setCurrentProjectId(issue.project_detail.id);
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
@ -87,47 +81,45 @@ export const IssueColumn: React.FC<Props> = ({
canEscapeKeyClose
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
content={
<div
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-100 bg-custom-background-90`}
>
<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">
<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={() => {
handleEditIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<Pencil className="h-4 w-4" />
<div className="flex items-center gap-2">
<Pencil className="h-3 w-3" />
<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"
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={() => {
handleDeleteIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<div className="flex items-center gap-2">
<Trash2 className="h-3 w-3" />
<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"
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={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<div className="flex items-center gap-2">
<LinkIcon className="h-3 w-3" />
<span>Copy issue link</span>
</div>
</button>

View File

@ -1,7 +1,7 @@
import React from "react";
// components
import { IssueColumn } from "components/core";
import { IssueColumn } from "components/issues";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
@ -14,7 +14,6 @@ type Props = {
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
properties: Properties;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
disableUserActions: boolean;
nestingLevel?: number;
};
@ -26,7 +25,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
setExpandedIssues,
properties,
handleIssueAction,
setCurrentProjectId,
disableUserActions,
nestingLevel = 0,
}) => {
@ -34,11 +32,10 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
setExpandedIssues((prevState) => {
const newArray = [...prevState];
const index = newArray.indexOf(issueId);
if (index > -1) {
newArray.splice(index, 1);
} else {
newArray.push(issueId);
}
if (index > -1) newArray.splice(index, 1);
else newArray.push(issueId);
return newArray;
});
};
@ -57,7 +54,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
properties={properties}
handleEditIssue={() => handleIssueAction(issue, "edit")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
setCurrentProjectId={setCurrentProjectId}
disableUserActions={disableUserActions}
nestingLevel={nestingLevel}
/>
@ -75,7 +71,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
setExpandedIssues={setExpandedIssues}
properties={properties}
handleIssueAction={handleIssueAction}
setCurrentProjectId={setCurrentProjectId}
disableUserActions={disableUserActions}
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";
// swr
import { observer } from "mobx-react-lite";
import { mutate } from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { IssueService } from "services/issue";
import { TrackEventService } from "services/track_event.service";
@ -28,181 +30,153 @@ export interface IIssueProperty {
const issueService = new IssueService();
const trackEventService = new TrackEventService();
export const IssueProperty: React.FC<IIssueProperty> = ({
workspaceSlug,
projectId,
parentIssue,
issue,
user,
editable,
}) => {
const [properties] = useIssuesProperties(workspaceSlug, projectId);
export const IssueProperty: React.FC<IIssueProperty> = observer(
({ workspaceSlug, projectId, parentIssue, issue, user, editable }) => {
const [properties] = useIssuesProperties(workspaceSlug, projectId);
const handlePriorityChange = (data: any) => {
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 { project: projectStore } = useMobxStore();
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,
});
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(
const handlePriorityChange = (data: any) => {
partialUpdateIssue({ priority: data });
trackEventService.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
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) => {
let newData = issue.assignees ?? [];
const handleStateChange = (data: IState) => {
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) {
if (newData.includes(data)) newData = newData.splice(newData.indexOf(data), 1);
else newData = [...newData, data];
} else newData = [...newData, data];
const handleAssigneeChange = (data: string[]) => {
partialUpdateIssue({ assignees_list: data, assignees: 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(
{
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 partialUpdateIssue = async (data: Partial<IIssue>) => {
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 partialUpdateIssue = async (data: Partial<IIssue>) => {
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);
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(
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
);
};
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}
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>
)}
</div>
)}
{properties.assignee && (
<div className="flex-shrink-0">
<MembersSelect
value={issue.assignees}
projectId={issue.project_detail.id}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
hideDropdownArrow
disabled={!editable}
/>
</div>
)}
</div>
);
};
{properties.state && (
<div className="flex-shrink-0">
<StateSelect
value={issue.state_detail}
stateGroups={projectStore.states ? projectStore.states[issue.project] : undefined}
onChange={(data) => handleStateChange(data)}
hideDropdownArrow
disabled={!editable}
/>
</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
import { CustomDatePicker } from "components/ui";
import { Tooltip } from "@plane/ui";
// helpers
import { findHowManyDaysLeft, renderShortDateWithYearFormat } from "helpers/date-time.helper";
// services
import { TrackEventService } from "services/track_event.service";
// types
import { IUser, IIssue } from "types";
import useIssuesView from "hooks/use-issues-view";
import { IIssue } from "types";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
onChange: (date: string | null) => void;
handleOnOpen?: () => void;
handleOnClose?: () => void;
tooltipPosition?: "top" | "bottom";
noBorder?: boolean;
user: IUser;
isNotAllowed: boolean;
disabled: boolean;
};
const trackEventService = new TrackEventService();
export const ViewDueDateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
onChange,
handleOnOpen,
handleOnClose,
tooltipPosition = "top",
noBorder = false,
user,
isNotAllowed,
disabled,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { displayFilters } = useIssuesView();
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
@ -59,34 +46,13 @@ export const ViewDueDateSelect: React.FC<Props> = ({
<CustomDatePicker
placeholder="Due date"
value={issue?.target_date}
onChange={(val) => {
partialUpdateIssue(
{
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"
}`}
onChange={onChange}
className={`bg-transparent ${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}`}
minDate={minDate ?? undefined}
noBorder={noBorder}
handleOnOpen={handleOnOpen}
handleOnClose={handleOnClose}
disabled={isNotAllowed}
disabled={disabled}
/>
</div>
</Tooltip>

View File

@ -1,7 +1,4 @@
import React from "react";
import { useRouter } from "next/router";
// services
import { TrackEventService } from "services/track_event.service";
// hooks
import useEstimateOption from "hooks/use-estimate-option";
// ui
@ -10,34 +7,23 @@ import { Tooltip } from "@plane/ui";
// icons
import { Triangle } from "lucide-react";
// types
import { IUser, IIssue } from "types";
import { IIssue } from "types";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
position?: "left" | "right";
onChange: (data: number) => void;
tooltipPosition?: "top" | "bottom";
selfPositioned?: boolean;
customButton?: boolean;
user: IUser | undefined;
isNotAllowed: boolean;
disabled: boolean;
};
const trackEventService = new TrackEventService();
export const ViewEstimateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
// position = "left",
onChange,
tooltipPosition = "top",
// selfPositioned = false,
customButton = false,
user,
isNotAllowed,
disabled,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { isEstimateActive, estimatePoints } = useEstimateOption(issue.estimate_point);
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
@ -45,7 +31,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
const estimateLabels = (
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>
<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"}
</div>
</Tooltip>
@ -56,31 +42,17 @@ export const ViewEstimateSelect: React.FC<Props> = ({
return (
<CustomSelect
value={issue.estimate_point}
onChange={(val: number) => {
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
);
}}
onChange={onChange}
{...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })}
maxHeight="md"
noChevron
disabled={isNotAllowed}
disabled={disabled}
width="w-full min-w-[8rem]"
>
<CustomSelect.Option value={null}>
<>
<span>
<Triangle className="h-4 w-4" />
<Triangle className="h-3 w-3" />
</span>
None
</>
@ -88,9 +60,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
{estimatePoints?.map((estimate) => (
<CustomSelect.Option key={estimate.id} value={estimate.key}>
<>
<span>
<Triangle className="h-4 w-4" />
</span>
<Triangle className="h-3 w-3" />
{estimate.value}
</>
</CustomSelect.Option>

View File

@ -1,44 +1,30 @@
import { FC } from "react";
import { useRouter } from "next/router";
// ui
import { CustomDatePicker } from "components/ui";
import { Tooltip } from "@plane/ui";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// services
import { TrackEventService } from "services/track_event.service";
// types
import { IUser, IIssue } from "types";
import useIssuesView from "hooks/use-issues-view";
import { IIssue } from "types";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
onChange: (date: string | null) => void;
handleOnOpen?: () => void;
handleOnClose?: () => void;
tooltipPosition?: "top" | "bottom";
noBorder?: boolean;
user: IUser | undefined;
isNotAllowed: boolean;
disabled: boolean;
};
const trackEventService = new TrackEventService();
export const ViewStartDateSelect: FC<Props> = ({
export const ViewStartDateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
onChange,
handleOnOpen,
handleOnClose,
tooltipPosition = "top",
noBorder = false,
user,
isNotAllowed,
disabled,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { displayFilters } = useIssuesView();
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
@ -52,34 +38,13 @@ export const ViewStartDateSelect: FC<Props> = ({
<CustomDatePicker
placeholder="Start date"
value={issue?.start_date}
onChange={(val) => {
partialUpdateIssue(
{
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"
}`}
onChange={onChange}
className={`bg-transparent ${issue?.start_date ? "w-[6.5rem]" : "w-[5rem] text-center"}`}
maxDate={maxDate ?? undefined}
noBorder={noBorder}
handleOnOpen={handleOnOpen}
handleOnClose={handleOnClose}
disabled={isNotAllowed}
disabled={disabled}
/>
</div>
</Tooltip>

View File

@ -1,31 +1,20 @@
import React, { useState } from "react";
import useSWR from "swr";
import { useRouter } from "next/router";
// 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";
// constants
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { Combobox } from "@headlessui/react";
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 = {
value: string[];
projectId: string;
onChange: (data: any) => void;
labelsDetails: any[];
onChange: (data: string[]) => void;
labels: IIssueLabels[];
className?: string;
buttonClassName?: string;
optionsClassName?: string;
@ -33,17 +22,12 @@ type Props = {
placement?: Placement;
hideDropdownArrow?: boolean;
disabled?: boolean;
user: IUser | undefined;
};
// services
const issueLabelService = new IssueLabelService();
export const LabelSelect: React.FC<Props> = ({
value,
projectId,
onChange,
labelsDetails,
labels,
className = "",
buttonClassName = "",
optionsClassName = "",
@ -51,31 +35,19 @@ export const LabelSelect: React.FC<Props> = ({
placement,
hideDropdownArrow = false,
disabled = false,
user,
}) => {
const [query, setQuery] = useState("");
const [fetchStates, setFetchStates] = useState(false);
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [labelModal, setLabelModal] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
});
const { data: issueLabels } = useSWR<IIssueLabels[]>(
projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId) : null,
workspaceSlug && projectId && fetchStates
? () => issueLabelService.getProjectIssueLabels(workspaceSlug.toString(), projectId)
: null
);
const options = issueLabels?.map((label) => ({
const options = labels?.map((label) => ({
value: label.id,
query: label.name,
content: (
@ -94,48 +66,6 @@ export const LabelSelect: React.FC<Props> = ({
const filteredOptions =
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 = (
<button
type="button"
@ -151,14 +81,15 @@ export const LabelSelect: React.FC<Props> = ({
return (
<>
{projectId && (
{/* TODO: update this logic */}
{/* {projectId && (
<CreateLabelModal
isOpen={labelModal}
handleClose={() => setLabelModal(false)}
projectId={projectId}
user={user}
/>
)}
)} */}
<Combobox
as="div"
className={`flex-shrink-0 text-left ${className}`}
@ -167,81 +98,116 @@ export const LabelSelect: React.FC<Props> = ({
disabled={disabled}
multiple
>
{({ open }: { open: boolean }) => {
if (open) setFetchStates(true);
return (
<>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs ${
disabled
? "cursor-not-allowed text-custom-text-200"
: value.length <= maxRender
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options>
<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}
style={styles.popper}
{...attributes.popper}
>
<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
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)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs ${
disabled
? "cursor-not-allowed text-custom-text-200"
: value.length <= maxRender
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
<div className={`flex items-center gap-2 text-custom-text-200`}>
{value.length > 0 ? (
value.length <= maxRender ? (
<>
{labels
.filter((l) => value.includes(l.id))
.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={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 className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
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>
</>
);
}}
)
) : (
""
)}
</div>
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options>
<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}
style={styles.popper}
{...attributes.popper}
>
<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
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)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
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>
</>
);

View File

@ -1,12 +1,7 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
// react-popper
import { usePopper } from "react-popper";
// hooks
import useProjectMembers from "hooks/use-project-members";
import useWorkspaceMembers from "hooks/use-workspace-members";
import { Placement } from "@popperjs/core";
// headless ui
import { Combobox } from "@headlessui/react";
// components
@ -14,65 +9,57 @@ import { AssigneesList, Avatar, Tooltip } from "components/ui";
// icons
import { Check, ChevronDown, Search, User2 } from "lucide-react";
// types
import { IUser } from "types";
import { Placement } from "@popperjs/core";
import { IUserLite } from "types";
type Props = {
value: string | string[];
projectId: string;
onChange: (data: any) => void;
membersDetails: IUser[];
renderWorkspaceMembers?: boolean;
members: IUserLite[];
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
hideDropdownArrow?: 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> = ({
value,
projectId,
onChange,
membersDetails,
renderWorkspaceMembers = false,
members,
className = "",
buttonClassName = "",
optionsClassName = "",
placement,
hideDropdownArrow = false,
disabled = false,
multiple = true,
}) => {
const [query, setQuery] = useState("");
const [fetchStates, setFetchStates] = useState(false);
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
});
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId, fetchStates && !renderWorkspaceMembers);
const { workspaceMembers } = useWorkspaceMembers(
workspaceSlug?.toString() ?? "",
fetchStates && renderWorkspaceMembers
);
const membersOptions = renderWorkspaceMembers ? workspaceMembers : members;
const options = membersOptions?.map((member) => ({
value: member.member.id,
query: member.member.display_name,
const options = members?.map((member) => ({
value: member.id,
query: member.display_name,
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.display_name}
<Avatar user={member} />
{member.display_name}
</div>
),
}));
@ -84,8 +71,11 @@ export const MembersSelect: React.FC<Props> = ({
<Tooltip
tooltipHeading="Assignee"
tooltipContent={
membersDetails && membersDetails.length > 0
? membersDetails.map((assignee) => assignee?.display_name).join(", ")
value && value.length > 0
? members
.filter((m) => value.includes(m.display_name))
.map((m) => m.display_name)
.join(", ")
: "No Assignee"
}
position="top"
@ -105,84 +95,72 @@ export const MembersSelect: React.FC<Props> = ({
</Tooltip>
);
return (
<Combobox
as="div"
className={`flex-shrink-0 text-left ${className}`}
value={value}
onChange={onChange}
disabled={disabled}
multiple
>
{({ open }: { open: boolean }) => {
if (open) setFetchStates(true);
const comboboxProps: any = { value, onChange, disabled };
if (multiple) comboboxProps.multiple = true;
return (
<>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
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"
} ${buttonClassName}`}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options>
<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}
style={styles.popper}
{...attributes.popper}
>
<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
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)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
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>
</div>
</Combobox.Options>
</>
);
}}
return (
<Combobox as="div" className={`flex-shrink-0 text-left ${className}`} {...comboboxProps}>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
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"
} ${buttonClassName}`}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options>
<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}
style={styles.popper}
{...attributes.popper}
>
<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
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)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
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>
</div>
</Combobox.Options>
</Combobox>
);
};

View File

@ -1,32 +1,21 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-popper
import { usePopper } from "react-popper";
// services
import { ProjectStateService } from "services/project";
// headless ui
import { Combobox } from "@headlessui/react";
// icons
import { Check, ChevronDown, Search } from "lucide-react";
// ui
import { StateGroupIcon } from "@plane/ui";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { Tooltip } from "components/ui";
import { Placement } from "@popperjs/core";
// constants
import { IState } from "types";
import { STATES_LIST } from "constants/fetch-keys";
// helper
import { getStatesList } from "helpers/state.helper";
import { IState, IStateResponse } from "types";
type Props = {
value: IState;
onChange: (data: any, states: IState[] | undefined) => void;
projectId: string;
onChange: (state: IState) => void;
stateGroups: IStateResponse | undefined;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
@ -35,13 +24,10 @@ type Props = {
disabled?: boolean;
};
// services
const projectStateService = new ProjectStateService();
export const StateSelect: React.FC<Props> = ({
value,
onChange,
projectId,
stateGroups,
className = "",
buttonClassName = "",
optionsClassName = "",
@ -54,22 +40,10 @@ export const StateSelect: React.FC<Props> = ({
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [fetchStates, setFetchStates] = useState<boolean>(false);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
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 options = states?.map((state) => ({
@ -101,79 +75,73 @@ export const StateSelect: React.FC<Props> = ({
className={`flex-shrink-0 text-left ${className}`}
value={value.id}
onChange={(data: string) => {
onChange(data, states);
const selectedState = states?.find((state) => state.id === data);
if (selectedState) onChange(selectedState);
}}
disabled={disabled}
>
{({ open }: { open: boolean }) => {
if (open) setFetchStates(true);
return (
<>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
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 ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options>
<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}
style={styles.popper}
{...attributes.popper}
>
<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
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)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
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>
</div>
</Combobox.Options>
</>
);
}}
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
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 ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options>
<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}
style={styles.popper}
{...attributes.popper}
>
<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
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)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
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>
</div>
</Combobox.Options>
</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 = [
{
propertyName: "title",
colName: "Title",
colSize: "440px",
export const SPREADSHEET_PROPERTY_DETAILS: {
[key: string]: {
title: string;
ascendingOrderKey: TIssueOrderByOptions;
ascendingOrderTitle: string;
descendingOrderKey: TIssueOrderByOptions;
descendingOrderTitle: string;
};
} = {
assignee: {
title: "Assignees",
ascendingOrderKey: "assignees__first_name",
ascendingOrderTitle: "A",
descendingOrderKey: "-assignees__first_name",
descendingOrderTitle: "Z",
},
{
propertyName: "state",
colName: "State",
colSize: "128px",
icon: Squares2X2Icon,
ascendingOrder: "state__name",
descendingOrder: "-state__name",
created_on: {
title: "Created on",
ascendingOrderKey: "-created_at",
ascendingOrderTitle: "New",
descendingOrderKey: "created_at",
descendingOrderTitle: "Old",
},
{
propertyName: "priority",
colName: "Priority",
colSize: "128px",
ascendingOrder: "priority",
descendingOrder: "-priority",
due_date: {
title: "Due date",
ascendingOrderKey: "-target_date",
ascendingOrderTitle: "New",
descendingOrderKey: "target_date",
descendingOrderTitle: "Old",
},
{
propertyName: "assignee",
colName: "Assignees",
colSize: "128px",
icon: UserGroupIcon,
ascendingOrder: "assignees__id",
descendingOrder: "-assignees__id",
estimate: {
title: "Estimate",
ascendingOrderKey: "estimate_point",
ascendingOrderTitle: "Low",
descendingOrderKey: "-estimate_point",
descendingOrderTitle: "High",
},
{
propertyName: "labels",
colName: "Labels",
colSize: "128px",
icon: TagIcon,
ascendingOrder: "labels__name",
descendingOrder: "-labels__name",
labels: {
title: "Labels",
ascendingOrderKey: "labels__name",
ascendingOrderTitle: "A",
descendingOrderKey: "-labels__name",
descendingOrderTitle: "Z",
},
{
propertyName: "start_date",
colName: "Start Date",
colSize: "128px",
icon: CalendarDaysIcon,
ascendingOrder: "-start_date",
descendingOrder: "start_date",
priority: {
title: "Priority",
ascendingOrderKey: "priority",
ascendingOrderTitle: "None",
descendingOrderKey: "-priority",
descendingOrderTitle: "Urgent",
},
{
propertyName: "due_date",
colName: "Due Date",
colSize: "128px",
icon: CalendarDaysIcon,
ascendingOrder: "-target_date",
descendingOrder: "target_date",
start_date: {
title: "Start date",
ascendingOrderKey: "-start_date",
ascendingOrderTitle: "New",
descendingOrderKey: "start_date",
descendingOrderTitle: "Old",
},
{
propertyName: "estimate",
colName: "Estimate",
colSize: "128px",
icon: PlayIcon,
ascendingOrder: "estimate_point",
descendingOrder: "-estimate_point",
state: {
title: "State",
ascendingOrderKey: "state__name",
ascendingOrderTitle: "A",
descendingOrderKey: "-state__name",
descendingOrderTitle: "Z",
},
{
propertyName: "created_on",
colName: "Created On",
colSize: "144px",
icon: CalendarDaysIcon,
ascendingOrder: "-created_at",
descendingOrder: "created_at",
updated_on: {
title: "Updated on",
ascendingOrderKey: "-updated_at",
ascendingOrderTitle: "New",
descendingOrderKey: "updated_at",
descendingOrderTitle: "Old",
},
{
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;
setPeekMode: (issueId: IPeekMode | null) => void;
// fetch issue details
fetchIssueDetails: (workspaceId: string, projectId: string, issueId: string) => void;
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => void;
// 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
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
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 {
@ -74,12 +74,12 @@ export class IssueDetailStore implements IIssueDetailStore {
setPeekMode = (mode: IPeekMode | null) => (this.peekMode = mode);
fetchIssueDetails = async (workspaceId: string, projectId: string, issueId: string) => {
fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
this.loader = true;
this.error = null;
const issueDetailsResponse = await this.issueService.retrieve(workspaceId, projectId, issueId);
const issueDetailsResponse = await this.issueService.retrieve(workspaceSlug, projectId, issueId);
runInAction(() => {
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 {
runInAction(() => {
this.loader = true;
this.error = null;
});
const response = await this.issueService.createIssues(workspaceId, projectId, data, user);
const response = await this.issueService.createIssues(workspaceSlug, projectId, data, user);
runInAction(() => {
this.loader = false;
@ -124,7 +124,7 @@ export class IssueDetailStore implements IIssueDetailStore {
};
updateIssue = async (
workspaceId: string,
workspaceSlug: string,
projectId: string,
issueId: string,
data: Partial<IIssue>,
@ -143,7 +143,7 @@ export class IssueDetailStore implements IIssueDetailStore {
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(() => {
this.loader = false;
@ -157,7 +157,7 @@ export class IssueDetailStore implements IIssueDetailStore {
};
});
} catch (error) {
this.fetchIssueDetails(workspaceId, projectId, issueId);
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
runInAction(() => {
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 };
delete newIssues[issueId];
@ -179,14 +179,14 @@ export class IssueDetailStore implements IIssueDetailStore {
this.issues = newIssues;
});
await this.issueService.deleteIssue(workspaceId, projectId, issueId, user);
await this.issueService.deleteIssue(workspaceSlug, projectId, issueId, user);
runInAction(() => {
this.loader = false;
this.error = null;
});
} catch (error) {
this.fetchIssueDetails(workspaceId, projectId, issueId);
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
runInAction(() => {
this.loader = false;

View File

@ -5,12 +5,14 @@ import { IssueService } from "services/issue";
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
// types
import { RootStore } from "../root";
import { IIssueFilterOptions } from "types";
import { IIssue, IIssueFilterOptions } from "types";
import {
IIssueGroupWithSubGroupsStructure,
IIssueGroupedStructure,
IIssueUnGroupedStructure,
} from "../module/module_issue.store";
// helpers
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
export interface IProjectViewIssuesStore {
// states
@ -27,6 +29,7 @@ export interface IProjectViewIssuesStore {
};
// actions
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
fetchViewIssues: (
workspaceSlug: string,
projectId: string,
@ -68,6 +71,7 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
viewIssues: observable.ref,
// actions
updateIssueStructure: action,
fetchViewIssues: action,
// computed
@ -99,6 +103,56 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
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) => {
try {
runInAction(() => {

View File

@ -139,7 +139,6 @@ class UserStore implements IUserStore {
try {
const response = await this.projectService.projectMemberMe(workspaceSlug, projectId);
console.log("response", response);
runInAction(() => {
this.projectMemberInfo = response;
this.hasPermissionToWorkspace = true;