refactor: folder structure, remove junk code (#2423)

* refactor: folder structure

* chore: ad order by target date option

* refactor: remove old layout components

* refactor: inbox folder structure
This commit is contained in:
Aaryan Khandelwal 2023-10-13 12:00:54 +05:30 committed by GitHub
parent 404e6a0cfc
commit 8aebf0bbd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
103 changed files with 112 additions and 5167 deletions

View File

@ -1,145 +0,0 @@
import { useRouter } from "next/router";
//hook
import useMyIssues from "hooks/my-issues/use-my-issues";
import useIssuesView from "hooks/use-issues-view";
import useProfileIssues from "hooks/use-profile-issues";
// components
import { SingleBoard } from "components/core/views/board-view/single-board";
import { IssuePeekOverview } from "components/issues";
// icons
import { StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
type Props = {
addIssueToGroup: (groupTitle: string) => void;
disableUserActions: boolean;
disableAddIssueOption?: boolean;
dragDisabled: boolean;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
myIssueProjectId?: string | null;
handleMyIssueOpen?: (issue: IIssue) => void;
states: IState[] | undefined;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
viewProps: IIssueViewProps;
};
export const AllBoards: React.FC<Props> = ({
addIssueToGroup,
disableUserActions,
disableAddIssueOption = false,
dragDisabled,
handleIssueAction,
handleDraftIssueAction,
handleTrashBox,
openIssuesListModal,
myIssueProjectId,
handleMyIssueOpen,
removeIssue,
states,
user,
userAuth,
viewProps,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, userId } = router.query;
const isProfileIssue =
router.pathname.includes("assigned") ||
router.pathname.includes("created") ||
router.pathname.includes("subscribed");
const isMyIssue = router.pathname.includes("my-issues");
const { mutateIssues } = useIssuesView();
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
const { displayFilters, groupedIssues } = viewProps;
return (
<>
<IssuePeekOverview
handleMutation={() => (isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues())}
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
{groupedIssues ? (
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-5 bg-custom-background-90">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0) return null;
return (
<SingleBoard
key={index}
addIssueToGroup={() => addIssueToGroup(singleGroup)}
currentState={currentState}
disableUserActions={disableUserActions}
disableAddIssueOption={disableAddIssueOption}
dragDisabled={dragDisabled}
groupTitle={singleGroup}
handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
handleTrashBox={handleTrashBox}
openIssuesListModal={openIssuesListModal ?? null}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={removeIssue}
user={user}
userAuth={userAuth}
viewProps={viewProps}
/>
);
})}
{!displayFilters?.show_empty_groups && (
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
<h2 className="text-lg font-semibold">Hidden groups</h2>
<div className="space-y-3">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (groupedIssues[singleGroup].length === 0)
return (
<div
key={index}
className="flex items-center justify-between gap-2 rounded bg-custom-background-100 p-2 shadow-custom-shadow-2xs"
>
<div className="flex items-center gap-2">
{currentState && (
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
)}
<h4 className="text-sm capitalize">
{displayFilters?.group_by === "state"
? addSpaceIfCamelCase(currentState?.name ?? "")
: addSpaceIfCamelCase(singleGroup)}
</h4>
</div>
<span className="text-xs text-custom-text-200">0</span>
</div>
);
})}
</div>
</div>
)}
</div>
) : null}
</>
);
};

View File

@ -1,214 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import issuesService from "services/issue.service";
import projectService from "services/project.service";
// hooks
import useProjects from "hooks/use-projects";
// component
import { Avatar, Icon } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, WORKSPACE_LABELS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
currentState?: IState | null;
groupTitle: string;
addIssueToGroup: () => void;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
disableUserActions: boolean;
disableAddIssue: boolean;
viewProps: IIssueViewProps;
};
export const BoardHeader: React.FC<Props> = ({
currentState,
groupTitle,
addIssueToGroup,
isCollapsed,
setIsCollapsed,
disableUserActions,
disableAddIssue,
viewProps,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { displayFilters, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR(
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: workspaceLabels } = useSWR(
workspaceSlug && displayFilters?.group_by === "labels" ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
workspaceSlug && displayFilters?.group_by === "labels"
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
: null
);
const { data: members } = useSWR(
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? PROJECT_MEMBERS(projectId.toString())
: null,
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
: null
);
const { projects } = useProjects();
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
switch (displayFilters?.group_by) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
case "labels":
title =
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find((label) => label.id === groupTitle)?.name ??
"None";
break;
case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
break;
case "assignees":
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
title = member ? member.display_name : "None";
break;
}
return title;
};
const getGroupIcon = () => {
let icon;
switch (displayFilters?.group_by) {
case "state":
icon = currentState && (
<StateGroupIcon stateGroup={currentState.group} color={currentState.color} height="16px" width="16px" />
);
break;
case "state_detail.group":
icon = (
<StateGroupIcon
stateGroup={groupTitle as TStateGroups}
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
height="16px"
width="16px"
/>
);
break;
case "priority":
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
break;
case "project":
const project = projects?.find((p) => p.id === groupTitle);
icon =
project &&
(project.emoji !== null
? renderEmoji(project.emoji)
: project.icon_prop !== null
? renderEmoji(project.icon_prop)
: null);
break;
case "labels":
const labelColor =
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find((label) => label.id === groupTitle)?.color ??
"#000000";
icon = <span className="h-3.5 w-3.5 flex-shrink-0 rounded-full" style={{ backgroundColor: labelColor }} />;
break;
case "assignees":
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
break;
}
return icon;
};
return (
<div
className={`flex items-center justify-between px-1 ${
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
}`}
>
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
<div
className={`flex cursor-pointer items-center gap-x-2 max-w-[316px] ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`}
>
<span className="flex items-center">{getGroupIcon()}</span>
<h2
className={`text-lg font-semibold truncate ${
displayFilters?.group_by === "created_by" ? "" : "capitalize"
}`}
style={{
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
}}
>
{getGroupTitle()}
</h2>
<span className={`${isCollapsed ? "ml-0.5" : ""} py-1 text-center text-sm`}>
{groupedIssues?.[groupTitle].length ?? 0}
</span>
</div>
</div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
>
{isCollapsed ? (
<Icon iconName="close_fullscreen" className="text-base font-medium text-custom-text-900" />
) : (
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
)}
</button>
{!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && (
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
onClick={addIssueToGroup}
>
<PlusIcon className="h-4 w-4" />
</button>
)}
</div>
</div>
);
};

View File

@ -1,5 +0,0 @@
export * from "./all-boards";
export * from "./board-header";
export * from "./single-board";
export * from "./single-issue";
export * from "./inline-create-issue-form";

View File

@ -1,62 +0,0 @@
import { useEffect } from "react";
// react hook form
import { useFormContext } from "react-hook-form";
// components
import { InlineCreateIssueFormWrapper } from "components/core";
// hooks
import useProjectDetails from "hooks/use-project-details";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<div>
<h4 className="text-sm font-medium leading-5 text-custom-text-300">
{projectDetails?.identifier ?? "..."}
</h4>
<input
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 pl-0 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</div>
);
};
export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
<>
<InlineCreateIssueFormWrapper
className="flex flex-col border-[0.5px] border-custom-border-100 justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow-custom-shadow-sm"
{...props}
>
<InlineInput />
</InlineCreateIssueFormWrapper>
{props.isOpen && (
<p className="text-xs ml-3 italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
</>
);

View File

@ -1,293 +0,0 @@
import { useState } from "react";
import { useRouter } from "next/router";
// react-beautiful-dnd
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable } from "react-beautiful-dnd";
// components
import { CreateUpdateDraftIssueModal } from "components/issues";
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
// ui
import { CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
type Props = {
addIssueToGroup: () => void;
currentState?: IState | null;
disableUserActions: boolean;
disableAddIssueOption?: boolean;
dragDisabled: boolean;
groupTitle: string;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
viewProps: IIssueViewProps;
};
export const SingleBoard: React.FC<Props> = (props) => {
const {
addIssueToGroup,
currentState,
groupTitle,
disableUserActions,
disableAddIssueOption = false,
dragDisabled,
handleIssueAction,
handleDraftIssueAction,
handleTrashBox,
openIssuesListModal,
handleMyIssueOpen,
removeIssue,
user,
userAuth,
viewProps,
} = props;
// collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true);
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
const [isCreateDraftIssueModalOpen, setIsCreateDraftIssueModalOpen] = useState(false);
const { displayFilters, groupedIssues } = viewProps;
const router = useRouter();
const { cycleId, moduleId } = router.query;
const isMyIssuesPage = router.pathname.split("/")[3] === "my-issues";
const isProfileIssuesPage = router.pathname.split("/")[2] === "profile";
const isDraftIssuesPage = router.pathname.split("/")[4] === "draft-issues";
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
const issuesLength = groupedIssues?.[groupTitle].length;
const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
const scrollToBottom = () => {
const boardListElement = document.getElementById(`board-list-${groupTitle}`);
// timeout is needed because the animation
// takes time to complete & we can scroll only after that
const timeoutId = setTimeout(() => {
if (boardListElement)
boardListElement.scrollBy({
top: boardListElement.scrollHeight,
left: 0,
behavior: "smooth",
});
clearTimeout(timeoutId);
}, 10);
};
const onCreateClick = () => {
setIsInlineCreateIssueFormOpen(true);
scrollToBottom();
};
const handleAddIssueToGroup = () => {
if (isDraftIssuesPage) setIsCreateDraftIssueModalOpen(true);
else if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
else onCreateClick();
};
return (
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
<CreateUpdateDraftIssueModal
isOpen={isCreateDraftIssueModalOpen}
handleClose={() => setIsCreateDraftIssueModalOpen(false)}
prePopulateData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
[displayFilters?.group_by! === "labels" ? "labels_list" : displayFilters?.group_by!]:
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
}}
/>
<BoardHeader
addIssueToGroup={handleAddIssueToGroup}
currentState={currentState}
groupTitle={groupTitle}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
disableUserActions={disableUserActions}
disableAddIssue={disableAddIssueOption}
viewProps={viewProps}
/>
{isCollapsed && (
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
className={`relative h-full ${
displayFilters?.order_by !== "sort_order" && snapshot.isDraggingOver
? "bg-custom-background-100/20"
: ""
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
ref={provided.innerRef}
{...provided.droppableProps}
>
{displayFilters?.order_by !== "sort_order" && (
<>
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-custom-background-90 opacity-50`}
/>
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-custom-background-100 p-2 text-xs`}
>
This board is ordered by{" "}
{replaceUnderscoreIfSnakeCase(
displayFilters?.order_by
? displayFilters?.order_by[0] === "-"
? displayFilters?.order_by.slice(1)
: displayFilters?.order_by
: "created_at"
)}
</div>
</>
)}
<div
id={`board-list-${groupTitle}`}
className={`pt-3 ${
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
} `}
>
{groupedIssues?.[groupTitle].map((issue, index) => (
<Draggable
key={issue.id}
draggableId={issue.id}
index={index}
isDragDisabled={isNotAllowed || dragDisabled}
>
{(provided, snapshot) => (
<SingleBoardIssue
key={index}
provided={provided}
snapshot={snapshot}
type={type}
index={index}
issue={issue}
projectId={issue.project_detail.id}
groupTitle={groupTitle}
editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDraftIssueEdit={
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "edit")
: undefined
}
handleDraftIssueDelete={() =>
handleDraftIssueAction
? handleDraftIssueAction(issue, "delete")
: undefined
}
handleTrashBox={handleTrashBox}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => {
if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
}}
disableUserActions={disableUserActions}
user={user}
userAuth={userAuth}
viewProps={viewProps}
/>
)}
</Draggable>
))}
<span
style={{
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
}}
>
<>{provided.placeholder}</>
</span>
<BoardInlineCreateIssueForm
isOpen={isInlineCreateIssueFormOpen}
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
onSuccess={() => scrollToBottom()}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
[displayFilters?.group_by! === "labels"
? "labels_list"
: displayFilters?.group_by!]:
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
}}
/>
</div>
{displayFilters?.group_by !== "created_by" && (
<div>
{type === "issue"
? !disableAddIssueOption &&
!isDraftIssuesPage && (
<button
type="button"
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
onClick={() => {
if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
else onCreateClick();
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
)
: !disableUserActions &&
!isDraftIssuesPage && (
<CustomMenu
customButton={
<button
type="button"
className="flex items-center gap-2 font-medium text-custom-primary outline-none whitespace-nowrap"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssuesPage) setIsCreateDraftIssueModalOpen(true);
else if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
else onCreateClick();
}}
>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
)}
</div>
)}
</StrictModeDroppable>
)}
</div>
);
};

View File

@ -1,527 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-beautiful-dnd
import { DraggableProvided, DraggableStateSnapshot, DraggingStyle, NotDraggingStyle } from "react-beautiful-dnd";
// services
import issuesService from "services/issue.service";
import trackEventServices from "services/track_event.service";
// hooks
import useToast from "hooks/use-toast";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
import { MembersSelect, LabelSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
// ui
import { ContextMenu, CustomMenu } from "components/ui";
import { Tooltip } from "@plane/ui";
// icons
import {
ClipboardDocumentCheckIcon,
LinkIcon,
PencilIcon,
TrashIcon,
XMarkIcon,
ArrowTopRightOnSquareIcon,
PaperClipIcon,
EllipsisHorizontalIcon,
} from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// helpers
import { handleIssuesMutation } from "helpers/issue.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// types
import {
ICurrentUserResponse,
IIssue,
IIssueViewProps,
IState,
ISubIssueResponse,
TIssuePriorities,
UserAuth,
} from "types";
// fetch-keys
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
type Props = {
type?: string;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
issue: IIssue;
projectId: string;
groupTitle?: string;
index: number;
editIssue: () => void;
makeIssueCopy: () => void;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleDraftIssueEdit?: () => void;
handleDraftIssueDelete?: () => void;
handleTrashBox: (isDragging: boolean) => void;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
viewProps: IIssueViewProps;
};
export const SingleBoardIssue: React.FC<Props> = ({
type,
provided,
snapshot,
issue,
projectId,
index,
editIssue,
makeIssueCopy,
handleMyIssueOpen,
removeIssue,
groupTitle,
handleDeleteIssue,
handleDraftIssueEdit,
handleDraftIssueDelete,
handleTrashBox,
disableUserActions,
user,
userAuth,
viewProps,
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const [isMenuActive, setIsMenuActive] = useState(false);
const [isDropdownActive, setIsDropdownActive] = useState(false);
const actionSectionRef = useRef<HTMLDivElement | null>(null);
const { displayFilters, properties, mutateIssues } = viewProps;
const router = useRouter();
const { workspaceSlug, cycleId, moduleId } = router.query;
const isDraftIssue = router.pathname.includes("draft-issues");
const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !issue) return;
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 {
mutateIssues(
(prevData: any) =>
handleIssuesMutation(
formData,
groupTitle ?? "",
displayFilters?.group_by ?? null,
index,
displayFilters?.order_by ?? "-created_at",
prevData
),
false
);
}
issuesService.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user).then(() => {
mutateIssues();
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
},
[displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
);
const getStyle = (style: DraggingStyle | NotDraggingStyle | undefined, snapshot: DraggableStateSnapshot) => {
if (displayFilters?.order_by === "sort_order") return style;
if (!snapshot.isDragging) return {};
if (!snapshot.isDropAnimating) return style;
return {
...style,
transitionDuration: `0.001s`,
};
};
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.",
});
setIsMenuActive(false);
});
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
partialUpdateIssue(
{
state: data,
state_detail: newState,
},
issue
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventServices.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user
);
}
};
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user
);
};
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user
);
};
useEffect(() => {
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
}, [snapshot, handleTrashBox]);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
const openPeekOverview = () => {
const { query } = router;
if (handleMyIssueOpen) handleMyIssueOpen(issue);
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
return (
<>
<ContextMenu
clickEvent={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
>
{!isNotAllowed && (
<>
<ContextMenu.Item
Icon={PencilIcon}
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else editIssue();
}}
>
Edit issue
</ContextMenu.Item>
{!isDraftIssue && (
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
)}
<ContextMenu.Item
Icon={TrashIcon}
onClick={() => {
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
else handleDeleteIssue(issue);
}}
>
Delete issue
</ContextMenu.Item>
</>
)}
{!isDraftIssue && (
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
)}
{!isDraftIssue && (
<a
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
target="_blank"
rel="noreferrer noopener"
>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>Open issue in new tab</ContextMenu.Item>
</a>
)}
</ContextMenu>
<div
className={`mb-3 rounded bg-custom-background-100 shadow ${
snapshot.isDragging ? "border-2 border-custom-primary shadow-lg" : ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getStyle(provided.draggableProps.style, snapshot)}
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition(e);
}}
>
<div className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px]">
{!isNotAllowed && (
<div
ref={actionSectionRef}
className={`z-1 absolute top-1.5 right-1.5 hidden group-hover/card:!flex ${isMenuActive ? "!flex" : ""}`}
>
{type && !isNotAllowed && (
<CustomMenu
customButton={
<button
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded p-1 text-left text-xs duration-300 hover:bg-custom-background-80"
onClick={() => setIsMenuActive(!isMenuActive)}
>
<EllipsisHorizontalIcon className="h-4 w-4" />
</button>
}
>
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else editIssue();
}}
>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && !isDraftIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<div className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove from {type}</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
else handleDeleteIssue(issue);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
{!isDraftIssue && (
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue Link</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
)}
<div className="flex flex-col gap-1.5">
{properties.key && (
<div className="text-xs font-medium text-custom-text-200">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<button
type="button"
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else openPeekOverview();
}}
>
<span className="text-sm text-left break-words line-clamp-2">{issue.name}</span>
</button>
</div>
<div className={`flex items-center gap-2 text-xs ${isDropdownActive ? "" : "overflow-x-scroll"}`}>
{properties.priority && (
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.state && (
<StateSelect
value={issue.state_detail}
onChange={handleStateChange}
projectId={projectId}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.start_date && issue.start_date && (
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
handleOnOpen={() => setIsDropdownActive(true)}
handleOnClose={() => setIsDropdownActive(false)}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
handleOnOpen={() => setIsDropdownActive(true)}
handleOnClose={() => setIsDropdownActive(false)}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && issue.labels.length > 0 && (
<LabelSelect
value={issue.labels}
projectId={projectId}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
user={user}
disabled={isNotAllowed}
/>
)}
{properties.assignee && (
<MembersSelect
value={issue.assignees}
projectId={projectId}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
user={user}
selfPositioned
/>
)}
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LayerDiagonalIcon className="h-3.5 w-3.5" />
{issue.sub_issues_count}
</div>
</Tooltip>
</div>
)}
{properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
</div>
</div>
</div>
</>
);
};

View File

@ -1,133 +0,0 @@
import React from "react";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// ui
import { CustomMenu } from "components/ui";
import { ToggleSwitch } from "@plane/ui";
// icons
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
// helpers
import { formatDate, isSameMonth, isSameYear, updateDateWithMonth, updateDateWithYear } from "helpers/calendar.helper";
// constants
import { MONTHS_LIST, YEARS_LIST } from "constants/calendar";
type Props = {
currentDate: Date;
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
showWeekEnds: boolean;
setShowWeekEnds: React.Dispatch<React.SetStateAction<boolean>>;
};
export const CalendarHeader: React.FC<Props> = ({ currentDate, setCurrentDate, showWeekEnds, setShowWeekEnds }) => (
<div className="mb-4 flex items-center justify-between">
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
<Popover className="flex h-full items-center justify-start rounded-lg">
{({ open }) => (
<>
<Popover.Button>
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
<span>{formatDate(currentDate, "Month")}</span> <span>{formatDate(currentDate, "yyyy")}</span>
</div>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
{YEARS_LIST.map((year) => (
<button
onClick={() => setCurrentDate(updateDateWithYear(year.label, currentDate))}
className={` ${
isSameYear(year.value, currentDate)
? "text-sm font-medium text-custom-text-100"
: "text-xs text-custom-text-200 "
} hover:text-sm hover:font-medium hover:text-custom-text-100`}
>
{year.label}
</button>
))}
</div>
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
{MONTHS_LIST.map((month) => (
<button
onClick={() => setCurrentDate(updateDateWithMonth(`${month.value}`, currentDate))}
className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
isSameMonth(`${month.value}`, currentDate) ? "font-medium text-custom-text-100" : ""
}`}
>
{month.label}
</button>
))}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<div className="flex items-center gap-2">
<button
className="cursor-pointer"
onClick={() => {
const previousMonthYear =
currentDate.getMonth() === 0 ? currentDate.getFullYear() - 1 : currentDate.getFullYear();
const previousMonthMonth = currentDate.getMonth() === 0 ? 11 : currentDate.getMonth() - 1;
const previousMonthFirstDate = new Date(previousMonthYear, previousMonthMonth, 1);
setCurrentDate(previousMonthFirstDate);
}}
>
<ChevronLeftIcon className="h-4 w-4" />
</button>
<button
className="cursor-pointer"
onClick={() => {
const nextMonthYear =
currentDate.getMonth() === 11 ? currentDate.getFullYear() + 1 : currentDate.getFullYear();
const nextMonthMonth = (currentDate.getMonth() + 1) % 12;
const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1);
setCurrentDate(nextMonthFirstDate);
}}
>
<ChevronRightIcon className="h-4 w-4" />
</button>
</div>
</div>
<div className="flex w-full items-center justify-end gap-2">
<button
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
onClick={() => setCurrentDate(new Date())}
>
Today
</button>
<CustomMenu
customButton={
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none">
Options
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
>
<div className="flex w-52 items-center justify-between px-1 text-sm text-custom-text-200">
<h4>Show weekends</h4>
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
</div>
</CustomMenu>
</div>
</div>
);
export default CalendarHeader;

View File

@ -1,203 +0,0 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { DragDropContext, DropResult } from "react-beautiful-dnd";
// services
import issuesService from "services/issue.service";
// components
import { SingleCalendarDate, CalendarHeader } from "components/core";
import { IssuePeekOverview } from "components/issues";
// ui
import { Spinner } from "@plane/ui";
// helpers
import { renderDateFormat } from "helpers/date-time.helper";
import { startOfWeek, lastDayOfWeek, eachDayOfInterval, weekDayInterval, formatDate } from "helpers/calendar.helper";
// types
import { ICalendarRange, ICurrentUserResponse, IIssue, UserAuth } from "types";
// fetch-keys
import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
VIEW_ISSUES,
} from "constants/fetch-keys";
type Props = {
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
addIssueToDate: (date: string) => void;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
};
export const CalendarView: React.FC<Props> = ({
handleIssueAction,
addIssueToDate,
disableUserActions,
user,
userAuth,
}) => {
const [showWeekEnds, setShowWeekEnds] = useState(false);
const { calendarIssues, mutateIssues, params, activeMonthDate, setActiveMonthDate } = useCalendarIssuesView();
const [calendarDates, setCalendarDates] = useState<ICalendarRange>({
startDate: startOfWeek(activeMonthDate),
endDate: lastDayOfWeek(activeMonthDate),
});
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const currentViewDays = showWeekEnds
? eachDayOfInterval({
start: calendarDates.startDate,
end: calendarDates.endDate,
})
: weekDayInterval({
start: calendarDates.startDate,
end: calendarDates.endDate,
});
const currentViewDaysData = currentViewDays.map((date: Date) => {
const filterIssue =
calendarIssues.length > 0
? calendarIssues.filter(
(issue) => issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
)
: [];
return {
date: renderDateFormat(date),
issues: filterIssue,
};
});
const weeks = ((date: Date[]) => {
const weeks = [];
if (showWeekEnds) {
for (let day = 0; day <= 6; day++) {
weeks.push(date[day]);
}
} else {
for (let day = 0; day <= 4; day++) {
weeks.push(date[day]);
}
}
return weeks;
})(currentViewDays);
const onDragEnd = (result: DropResult) => {
const { source, destination, draggableId } = result;
if (!destination || !workspaceSlug || !projectId) return;
if (source.droppableId === destination.droppableId) 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.toString(), params);
mutate<IIssue[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === draggableId)
return {
...p,
target_date: destination.droppableId,
};
return p;
}),
false
);
issuesService
.patchIssue(
workspaceSlug as string,
projectId as string,
draggableId,
{
target_date: destination?.droppableId,
},
user
)
.then(() => mutate(fetchKey));
};
useEffect(() => {
setCalendarDates({
startDate: startOfWeek(activeMonthDate),
endDate: lastDayOfWeek(activeMonthDate),
});
}, [activeMonthDate]);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
return (
<>
<IssuePeekOverview
handleMutation={() => mutateIssues()}
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
{calendarIssues ? (
<div className="h-full overflow-y-auto">
<DragDropContext onDragEnd={onDragEnd}>
<div
id={`calendar-view-${cycleId ?? moduleId ?? viewId ?? ""}`}
className="h-full rounded-lg p-8 text-custom-text-200"
>
<CalendarHeader
showWeekEnds={showWeekEnds}
setShowWeekEnds={setShowWeekEnds}
currentDate={activeMonthDate}
setCurrentDate={setActiveMonthDate}
/>
<div
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
}`}
>
{weeks.map((date, index) => (
<div
key={index}
className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200`}
>
<span>{formatDate(date, "eee").substring(0, 3)}</span>
</div>
))}
</div>
<div className={`grid h-full auto-rows-min ${showWeekEnds ? "grid-cols-7" : "grid-cols-5"} `}>
{currentViewDaysData.map((date, index) => (
<SingleCalendarDate
key={`${date}-${index}`}
index={index}
date={date}
handleIssueAction={handleIssueAction}
addIssueToDate={addIssueToDate}
showWeekEnds={showWeekEnds}
user={user}
isNotAllowed={isNotAllowed}
/>
))}
</div>
</div>
</DragDropContext>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</>
);
};

View File

@ -1,5 +0,0 @@
export * from "./calendar-header";
export * from "./calendar";
export * from "./single-date";
export * from "./single-issue";
export * from "./inline-create-issue-form";

View File

@ -1,102 +0,0 @@
import { useEffect, useRef, useState } from "react";
// next
import { useRouter } from "next/router";
// react hook form
import { useFormContext } from "react-hook-form";
import { InlineCreateIssueFormWrapper } from "components/core";
// hooks
import useProjectDetails from "hooks/use-project-details";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
dependencies: any[];
};
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>, deps: any[]) => {
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
const router = useRouter();
const { moduleId, cycleId, viewId } = router.query;
const container = document.getElementById(`calendar-view-${cycleId ?? moduleId ?? viewId}`);
useEffect(() => {
if (!ref.current) return;
const { right } = ref.current.getBoundingClientRect();
const width = right;
const innerWidth = container?.getBoundingClientRect().width ?? window.innerWidth;
if (width > innerWidth) setIsThereSpaceOnRight(false);
else setIsThereSpaceOnRight(true);
}, [ref, deps, container]);
return isThereSpaceOnRight;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<>
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
{projectDetails?.identifier ?? "..."}
</h4>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full pr-2 py-2.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</>
);
};
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
const { isOpen, dependencies } = props;
const ref = useRef<HTMLDivElement>(null);
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies);
return (
<>
<div
ref={ref}
className={`absolute top-10 transition-all z-20 w-full max-w-[calc(100%-1.25rem)] ${
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
} right-2.5`}
>
<InlineCreateIssueFormWrapper
{...props}
className="flex w-full px-1.5 border-[0.5px] border-custom-border-100 rounded z-50 items-center gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm transition-opacity"
>
<InlineInput />
</InlineCreateIssueFormWrapper>
</div>
{/* Added to make any other element as outside click. This will make input also to be outside. */}
{isOpen && <div className="w-screen h-screen fixed inset-0 z-10" />}
</>
);
};

View File

@ -1,126 +0,0 @@
import React, { useState } from "react";
// next
import { useRouter } from "next/router";
// react-beautiful-dnd
import { Draggable } from "react-beautiful-dnd";
// component
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { SingleCalendarIssue } from "./single-issue";
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
// icons
import { PlusSmallIcon } from "@heroicons/react/24/outline";
// helper
import { formatDate } from "helpers/calendar.helper";
// types
import { ICurrentUserResponse, IIssue } from "types";
type Props = {
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
index: number;
date: {
date: string;
issues: IIssue[];
};
addIssueToDate: (date: string) => void;
showWeekEnds: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
export const SingleCalendarDate: React.FC<Props> = (props) => {
const { handleIssueAction, date, index, showWeekEnds, user, isNotAllowed } = props;
const router = useRouter();
const { cycleId, moduleId } = router.query;
const [showAllIssues, setShowAllIssues] = useState(false);
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
const [formPosition, setFormPosition] = useState({ x: 0, y: 0 });
const totalIssues = date.issues.length;
return (
<StrictModeDroppable droppableId={date.date}>
{(provided) => (
<div
key={index}
ref={provided.innerRef}
{...provided.droppableProps}
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-custom-border-200 p-2.5 text-left text-sm font-medium hover:bg-custom-background-90 ${
showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? ""
: "border-r"
}`}
>
<>
<span>{formatDate(new Date(date.date), "d")}</span>
{totalIssues > 0 &&
date.issues.slice(0, showAllIssues ? totalIssues : 4).map((issue: IIssue, index) => (
<Draggable key={issue.id} draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<SingleCalendarIssue
key={index}
index={index}
provided={provided}
snapshot={snapshot}
issue={issue}
projectId={issue.project_detail.id}
handleEditIssue={() => handleIssueAction(issue, "edit")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
</Draggable>
))}
<CalendarInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
dependencies={[showWeekEnds]}
handleClose={() => setIsCreateIssueFormOpen(false)}
prePopulatedData={{
target_date: date.date,
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/>
{totalIssues > 4 && (
<button
type="button"
className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 px-1.5 py-1 text-xs"
onClick={() => setShowAllIssues((prevData) => !prevData)}
>
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
</button>
)}
<div
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-custom-background-80 p-1 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100`}
>
<button
onClick={(e) => {
setIsCreateIssueFormOpen(true);
setFormPosition({ x: e.clientX, y: e.clientY });
}}
className="flex items-center justify-center gap-1 text-center"
>
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
Add issue
</button>
</div>
{provided.placeholder}
</>
</div>
)}
</StrictModeDroppable>
);
};

View File

@ -1,389 +0,0 @@
import React, { useCallback } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-beautiful-dnd
import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
// services
import issuesService from "services/issue.service";
import trackEventServices from "services/track_event.service";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useToast from "hooks/use-toast";
// components
import { CustomMenu } from "components/ui";
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
import { Tooltip } from "@plane/ui";
// icons
import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// helper
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// type
import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse, TIssuePriorities } from "types";
// fetch-keys
import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES,
VIEW_ISSUES,
} from "constants/fetch-keys";
type Props = {
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
index: number;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
issue: IIssue;
projectId: string;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
export const SingleCalendarIssue: React.FC<Props> = ({
handleEditIssue,
handleDeleteIssue,
index,
provided,
snapshot,
issue,
projectId,
user,
isNotAllowed,
}) => {
const router = useRouter();
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
const { setToastAlert } = useToast();
const params = {};
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
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.toString(), 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,
assignees: formData?.assignees_list ?? p.assignees,
};
}
return p;
}),
false
);
}
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id as string, formData, user)
.then(() => {
mutate(fetchKey);
})
.catch((error) => {
console.log(error);
});
},
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
);
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
partialUpdateIssue(
{
state: data,
state_detail: newState,
},
issue
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventServices.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user
);
}
};
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user
);
};
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user
);
};
const displayProperties = properties ? Object.values(properties).some((value) => value === true) : false;
const openPeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
return (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`w-full relative cursor-pointer rounded border border-custom-border-200 px-1.5 py-1.5 text-xs duration-300 hover:cursor-move hover:bg-custom-background-80 ${
snapshot.isDragging ? "bg-custom-background-80 shadow-lg" : ""
}`}
>
<div className="group/card flex w-full flex-col items-start justify-center gap-1.5 text-xs sm:w-auto ">
{!isNotAllowed && (
<div className="z-1 absolute top-1.5 right-1.5 opacity-0 group-hover/card:opacity-100">
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={() => handleEditIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue Link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
)}
<button
type="button"
className="flex w-full cursor-pointer flex-col items-start justify-center gap-1.5"
onClick={openPeekOverview}
>
{properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-xs text-custom-text-100">{truncateText(issue.name, 25)}</span>
</Tooltip>
</button>
{displayProperties && (
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.state && (
<StateSelect
value={issue.state_detail}
onChange={handleStateChange}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.start_date && issue.start_date && (
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && issue.labels.length > 0 && (
<LabelSelect
value={issue.labels}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
maxRender={1}
user={user}
disabled={isNotAllowed}
/>
)}
{properties.assignee && (
<MembersSelect
value={issue.assignees}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LayerDiagonalIcon className="h-3.5 w-3.5" />
{issue.sub_issues_count}
</div>
</Tooltip>
</div>
)}
{properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
</div>
)}
</div>
</div>
);
};

View File

@ -1,30 +0,0 @@
import { useRouter } from "next/router";
// components
import { CycleIssuesGanttChartView } from "components/cycles";
import { IssueGanttChartView } from "components/issues";
import { ModuleIssuesGanttChartView } from "components/modules";
import { ViewIssuesGanttChartView } from "components/views";
type Props = {
disableUserActions: boolean;
};
export const GanttChartView: React.FC<Props> = ({ disableUserActions }) => {
const router = useRouter();
const { cycleId, moduleId, viewId } = router.query;
return (
<>
{cycleId ? (
<CycleIssuesGanttChartView disableUserActions={disableUserActions} />
) : moduleId ? (
<ModuleIssuesGanttChartView disableUserActions={disableUserActions} />
) : viewId ? (
<ViewIssuesGanttChartView disableUserActions={disableUserActions} />
) : (
<IssueGanttChartView disableUserActions={disableUserActions} />
)}
</>
);
};

View File

@ -1,62 +0,0 @@
import { useEffect } from "react";
// react hook form
import { useFormContext } from "react-hook-form";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components
import { InlineCreateIssueFormWrapper } from "components/core";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<>
<div className="w-[14px] h-[14px] rounded-full border border-custom-border-1000 flex-shrink-0" />
<h4 className="text-sm text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</>
);
};
export const GanttInlineCreateIssueForm: React.FC<Props> = (props) => (
<>
<InlineCreateIssueFormWrapper
className="flex py-3 px-4 border-[0.5px] border-custom-border-100 mr-2.5 items-center rounded gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm"
{...props}
>
<InlineInput />
</InlineCreateIssueFormWrapper>
{props.isOpen && (
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
</>
);

View File

@ -1,8 +1,3 @@
export * from "./board-view";
export * from "./calendar-view";
export * from "./gantt-chart-view";
export * from "./list-view";
export * from "./spreadsheet-view";
export * from "./all-views";
export * from "./issues-view";
export * from "./inline-issue-create-wrapper";

View File

@ -1,104 +0,0 @@
import { useRouter } from "next/router";
// hooks
import useMyIssues from "hooks/my-issues/use-my-issues";
import useIssuesView from "hooks/use-issues-view";
import useProfileIssues from "hooks/use-profile-issues";
// components
import { SingleList } from "components/core/views/list-view/single-list";
import { IssuePeekOverview } from "components/issues";
// types
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
// types
type Props = {
states: IState[] | undefined;
addIssueToGroup: (groupTitle: string) => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
openIssuesListModal?: (() => void) | null;
myIssueProjectId?: string | null;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
disableUserActions: boolean;
disableAddIssueOption?: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
viewProps: IIssueViewProps;
};
export const AllLists: React.FC<Props> = ({
addIssueToGroup,
handleIssueAction,
disableUserActions,
disableAddIssueOption = false,
openIssuesListModal,
handleMyIssueOpen,
myIssueProjectId,
removeIssue,
states,
handleDraftIssueAction,
user,
userAuth,
viewProps,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, userId } = router.query;
const isProfileIssue =
router.pathname.includes("assigned") ||
router.pathname.includes("created") ||
router.pathname.includes("subscribed");
const isMyIssue = router.pathname.includes("my-issues");
const { mutateIssues } = useIssuesView();
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
const { displayFilters, groupedIssues } = viewProps;
return (
<>
<IssuePeekOverview
handleMutation={() =>
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
}
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
{groupedIssues && (
<div className="h-full overflow-y-auto">
{Object.keys(groupedIssues).map((singleGroup) => {
const currentState =
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
return null;
return (
<SingleList
key={singleGroup}
groupTitle={singleGroup}
currentState={currentState}
addIssueToGroup={() => addIssueToGroup(singleGroup)}
handleDraftIssueAction={handleDraftIssueAction}
handleIssueAction={handleIssueAction}
handleMyIssueOpen={handleMyIssueOpen}
openIssuesListModal={openIssuesListModal}
removeIssue={removeIssue}
disableUserActions={disableUserActions}
disableAddIssueOption={disableAddIssueOption}
user={user}
userAuth={userAuth}
viewProps={viewProps}
/>
);
})}
</div>
)}
</>
);
};

View File

@ -1,4 +0,0 @@
export * from "./all-lists";
export * from "./single-issue";
export * from "./single-list";
export * from "./inline-create-issue-form";

View File

@ -1,62 +0,0 @@
import { useEffect } from "react";
// react hook form
import { useFormContext } from "react-hook-form";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components
import { InlineCreateIssueFormWrapper } from "../inline-issue-create-wrapper";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<>
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
{projectDetails?.identifier ?? "..."}
</h4>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 py-3 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</>
);
};
export const ListInlineCreateIssueForm: React.FC<Props> = (props) => (
<>
<InlineCreateIssueFormWrapper
className="flex border-[0.5px] border-t-0 border-custom-border-100 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-sm z-10"
{...props}
>
<InlineInput />
</InlineCreateIssueFormWrapper>
{props.isOpen && (
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
</>
);

View File

@ -1,482 +0,0 @@
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// services
import issuesService from "services/issue.service";
import trackEventServices from "services/track_event.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
// ui
import { CustomMenu, ContextMenu } from "components/ui";
import { Tooltip } from "@plane/ui";
// icons
import {
ClipboardDocumentCheckIcon,
LinkIcon,
PencilIcon,
TrashIcon,
XMarkIcon,
ArrowTopRightOnSquareIcon,
PaperClipIcon,
} from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { handleIssuesMutation } from "helpers/issue.helper";
// types
import {
ICurrentUserResponse,
IIssue,
IIssueViewProps,
IState,
ISubIssueResponse,
IUserProfileProjectSegregation,
TIssuePriorities,
UserAuth,
} from "types";
// fetch-keys
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES, USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
type Props = {
type?: string;
issue: IIssue;
projectId: string;
groupTitle?: string;
editIssue: () => void;
index: number;
makeIssueCopy: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleDraftIssueSelect?: (issue: IIssue) => void;
handleDraftIssueDelete?: (issue: IIssue) => void;
handleMyIssueOpen?: (issue: IIssue) => void;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
viewProps: IIssueViewProps;
};
export const SingleListIssue: React.FC<Props> = ({
type,
issue,
projectId,
editIssue,
index,
makeIssueCopy,
removeIssue,
groupTitle,
handleDraftIssueDelete,
handleDeleteIssue,
handleMyIssueOpen,
disableUserActions,
user,
userAuth,
viewProps,
handleDraftIssueSelect,
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const router = useRouter();
const { workspaceSlug, cycleId, moduleId, userId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
const { setToastAlert } = useToast();
const { displayFilters, properties, mutateIssues } = viewProps;
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !issue) return;
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 {
mutateIssues(
(prevData: any) =>
handleIssuesMutation(
formData,
groupTitle ?? "",
displayFilters?.group_by ?? null,
index,
displayFilters?.order_by ?? "-created_at",
prevData
),
false
);
}
issuesService.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user).then(() => {
mutateIssues();
if (userId)
mutate<IUserProfileProjectSegregation>(
USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
);
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
},
[displayFilters, workspaceSlug, cycleId, moduleId, userId, groupTitle, index, mutateIssues, user]
);
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const handleStateChange = (data: string, states: IState[] | undefined) => {
const oldState = states?.find((s) => s.id === issue.state);
const newState = states?.find((s) => s.id === data);
partialUpdateIssue(
{
state: data,
state_detail: newState,
},
issue
);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE",
user
);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventServices.trackIssueMarkedAsDoneEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
user
);
}
};
const handleAssigneeChange = (data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
user
);
};
const handleLabelChange = (data: any) => {
partialUpdateIssue({ labels_list: data }, issue);
};
const handlePriorityChange = (data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug,
workspaceId: issue.workspace,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY",
user
);
};
const issuePath = isArchivedIssues
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
: isDraftIssues
? `#`
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
const openPeekOverview = (issue: IIssue) => {
const { query } = router;
if (handleMyIssueOpen) handleMyIssueOpen(issue);
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
return (
<>
<ContextMenu
clickEvent={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
>
{!isNotAllowed && (
<>
<ContextMenu.Item
Icon={PencilIcon}
onClick={() => {
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
else editIssue();
}}
>
Edit issue
</ContextMenu.Item>
{!isDraftIssues && (
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
)}
<ContextMenu.Item
Icon={TrashIcon}
onClick={() => {
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
else handleDeleteIssue(issue);
}}
>
Delete issue
</ContextMenu.Item>
</>
)}
{!isDraftIssues && (
<>
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
<a href={issuePath} target="_blank" rel="noreferrer noopener">
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>Open issue in new tab</ContextMenu.Item>
</a>
</>
)}
</ContextMenu>
<div
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b-[0.5px] border-custom-border-100 bg-custom-background-100 last:border-b-0"
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition(e);
}}
>
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
<div className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<button
type="button"
className="truncate text-[0.825rem] text-custom-text-100"
onClick={() => {
if (isArchivedIssues) return router.push(issuePath);
if (!isDraftIssues) openPeekOverview(issue);
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
}}
>
{issue.name}
</button>
</Tooltip>
</div>
</div>
<div className={`flex flex-shrink-0 items-center gap-2 text-xs ${isArchivedIssues ? "opacity-60" : ""}`}>
{properties.priority && (
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.state && (
<StateSelect
value={issue.state_detail}
onChange={handleStateChange}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.start_date && issue.start_date && (
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && (
<LabelSelect
value={issue.labels}
onChange={handleLabelChange}
labelsDetails={issue.label_details}
hideDropdownArrow
maxRender={3}
user={user}
disabled={isNotAllowed}
/>
)}
{properties.assignee && (
<MembersSelect
value={issue.assignees}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
hideDropdownArrow
disabled={isNotAllowed}
/>
)}
{properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LayerDiagonalIcon className="h-3.5 w-3.5" />
{issue.sub_issues_count}
</div>
</Tooltip>
</div>
)}
{properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
else editIssue();
}}
>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<div className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove from {type}</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={() => {
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
else handleDeleteIssue(issue);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
{!isDraftIssues && (
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
</div>
</>
);
};

View File

@ -1,349 +0,0 @@
import { useState } from "react";
// next
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// services
import issuesService from "services/issue.service";
import projectService from "services/project.service";
// hooks
import useProjects from "hooks/use-projects";
// components
import { CreateUpdateDraftIssueModal } from "components/issues";
import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
// ui
import { Avatar, CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import {
ICurrentUserResponse,
IIssue,
IIssueLabels,
IIssueViewProps,
IState,
TIssuePriorities,
TStateGroups,
UserAuth,
} from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, WORKSPACE_LABELS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
currentState?: IState | null;
groupTitle: string;
addIssueToGroup: () => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
openIssuesListModal?: (() => void) | null;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
disableUserActions: boolean;
disableAddIssueOption?: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
viewProps: IIssueViewProps;
};
export const SingleList: React.FC<Props> = (props) => {
const {
currentState,
groupTitle,
handleIssueAction,
openIssuesListModal,
handleDraftIssueAction,
handleMyIssueOpen,
addIssueToGroup,
removeIssue,
disableUserActions,
disableAddIssueOption = false,
user,
userAuth,
viewProps,
} = props;
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
const [isDraftIssuesModalOpen, setIsDraftIssuesModalOpen] = useState(false);
const isMyIssuesPage = router.pathname.split("/")[3] === "my-issues";
const isProfileIssuesPage = router.pathname.split("/")[2] === "profile";
const isDraftIssuesPage = router.pathname.split("/")[4] === "draft-issues";
const isArchivedIssues = router.pathname.includes("archived-issues");
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const { displayFilters, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR(
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: workspaceLabels } = useSWR(
workspaceSlug && displayFilters?.group_by === "labels" ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
workspaceSlug && displayFilters?.group_by === "labels"
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
: null
);
const { data: members } = useSWR(
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? PROJECT_MEMBERS(projectId as string)
: null,
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const { projects } = useProjects();
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
switch (displayFilters?.group_by) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
case "labels":
title =
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find((label) => label.id === groupTitle)?.name ??
"None";
break;
case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
break;
case "assignees":
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
title = member ? member.display_name : "None";
break;
}
return title;
};
const getGroupIcon = () => {
let icon;
switch (displayFilters?.group_by) {
case "state":
icon = currentState && (
<StateGroupIcon stateGroup={currentState.group} color={currentState.color} height="16px" width="16px" />
);
break;
case "state_detail.group":
icon = (
<StateGroupIcon
stateGroup={groupTitle as TStateGroups}
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
height="16px"
width="16px"
/>
);
break;
case "priority":
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
break;
case "project":
const project = projects?.find((p) => p.id === groupTitle);
icon =
project &&
(project.emoji !== null
? renderEmoji(project.emoji)
: project.icon_prop !== null
? renderEmoji(project.icon_prop)
: null);
break;
case "labels":
const labelColor =
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find((label) => label.id === groupTitle)?.color ??
"#000000";
icon = <span className="h-3 w-3 flex-shrink-0 rounded-full" style={{ backgroundColor: labelColor }} />;
break;
case "assignees":
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
break;
}
return icon;
};
if (!groupedIssues) return null;
return (
<>
<CreateUpdateDraftIssueModal
isOpen={isDraftIssuesModalOpen}
handleClose={() => setIsDraftIssuesModalOpen(false)}
prePopulateData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
[displayFilters?.group_by! === "labels" ? "labels_list" : displayFilters?.group_by!]:
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
}}
/>
<Disclosure as="div" defaultOpen>
{({ open }) => (
<div>
<div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
<Disclosure.Button>
<div className="flex items-center gap-x-3">
{displayFilters?.group_by !== null && <div className="flex items-center">{getGroupIcon()}</div>}
{displayFilters?.group_by !== null ? (
<h2
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
displayFilters?.group_by === "created_by" ? "" : "capitalize"
}`}
>
{getGroupTitle()}
</h2>
) : (
<h2 className="font-medium leading-5">All Issues</h2>
)}
<span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs">
{groupedIssues[groupTitle as keyof IIssue].length}
</span>
</div>
</Disclosure.Button>
{isArchivedIssues ? (
""
) : type === "issue" ? (
!disableAddIssueOption && (
<button
type="button"
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
if (isDraftIssuesPage) setIsDraftIssuesModalOpen(true);
else if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
else setIsCreateIssueFormOpen(true);
}}
>
<PlusIcon className="h-4 w-4" />
</button>
)
) : disableUserActions ? (
""
) : (
<CustomMenu
customButton={
<div className="flex cursor-pointer items-center">
<PlusIcon className="h-4 w-4" />
</div>
}
position="right"
noBorder
>
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>Create new</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
{groupedIssues[groupTitle] ? (
groupedIssues[groupTitle].length > 0 ? (
groupedIssues[groupTitle].map((issue, index) => (
<SingleListIssue
key={issue.id}
type={type}
issue={issue}
projectId={issue.project_detail.id}
groupTitle={groupTitle}
index={index}
editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDraftIssueSelect={
handleDraftIssueAction ? () => handleDraftIssueAction(issue, "edit") : undefined
}
handleDraftIssueDelete={
handleDraftIssueAction ? () => handleDraftIssueAction(issue, "delete") : undefined
}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => {
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id);
}}
disableUserActions={disableUserActions}
user={user}
userAuth={userAuth}
viewProps={viewProps}
/>
))
) : (
<p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">No issues.</p>
)
) : (
<div className="flex h-full w-full items-center justify-center">Loading...</div>
)}
<ListInlineCreateIssueForm
isOpen={isCreateIssueFormOpen && !disableAddIssueOption}
handleClose={() => setIsCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
[displayFilters?.group_by! === "labels" ? "labels_list" : displayFilters?.group_by!]:
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
}}
/>
{!disableAddIssueOption && !isCreateIssueFormOpen && !isDraftIssuesPage && (
<div className="w-full bg-custom-background-100 px-6 py-3 border-b border-custom-border-100">
<button
type="button"
onClick={() => {
if (isDraftIssuesPage) setIsDraftIssuesModalOpen(true);
else if (isMyIssuesPage || isProfileIssuesPage) addIssueToGroup();
else setIsCreateIssueFormOpen(true);
}}
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
>
<PlusIcon className="h-4 w-4" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</button>
</div>
)}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</>
);
};

View File

@ -1,61 +0,0 @@
import { useRouter } from "next/router";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user";
import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view";
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
// types
import { IIssue } from "types";
type Props = {
disableUserActions: boolean;
};
export const CycleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const { displayFilters } = useIssuesView();
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { ganttIssues, mutateGanttIssues } = useGanttChartCycleIssues(
workspaceSlug as string,
projectId as string,
cycleId as string
);
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
handleMutation={() => mutateGanttIssues()}
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) => {}}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
bottomSpacing
/>
</div>
</>
);
};

View File

@ -1,3 +1,2 @@
export * from "./blocks";
export * from "./cycle-issues-layout";
export * from "./cycles-list-layout";

View File

@ -19,17 +19,17 @@ import { PlusIcon } from "lucide-react";
import {
// generateHourChart,
// generateDayChart,
generateWeekChart,
generateBiWeekChart,
// generateWeekChart,
// generateBiWeekChart,
generateMonthChart,
generateQuarterChart,
generateYearChart,
// generateQuarterChart,
// generateYearChart,
getNumberOfDaysBetweenTwoDatesInMonth,
getNumberOfDaysBetweenTwoDatesInQuarter,
getNumberOfDaysBetweenTwoDatesInYear,
// getNumberOfDaysBetweenTwoDatesInQuarter,
// getNumberOfDaysBetweenTwoDatesInYear,
getMonthChartItemPositionWidthInMonth,
} from "../views";
import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form";
// import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form";
// types
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
// data
@ -74,8 +74,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
// blocks state management starts
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } =
useChart();
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart();
const router = useRouter();
const { cycleId, moduleId } = router.query;
@ -92,8 +91,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
: [];
useEffect(() => {
if (currentViewData && blocks)
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
if (currentViewData && blocks) setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
}, [currentViewData, blocks]);
// blocks state management ends
@ -115,8 +113,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
// if (view === "day") currentRender = generateDayChart(selectedCurrentViewData, side);
// if (view === "week") currentRender = generateWeekChart(selectedCurrentViewData, side);
// if (view === "bi_week") currentRender = generateBiWeekChart(selectedCurrentViewData, side);
if (selectedCurrentView === "month")
currentRender = generateMonthChart(selectedCurrentViewData, side);
if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side);
// if (view === "quarter") currentRender = generateQuarterChart(selectedCurrentViewData, side);
// if (selectedCurrentView === "year")
// currentRender = generateYearChart(selectedCurrentViewData, side);
@ -155,10 +152,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
});
setItemsContainerWidth(currentRender.scrollWidth);
setTimeout(() => {
handleScrollToCurrentSelectedDate(
currentRender.state,
currentRender.state.data.currentDate
);
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
}, 50);
}
}
@ -202,8 +196,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
// if (currentView === "year")
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
scrollWidth =
daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width);
scrollWidth = daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width);
scrollContainer.scrollLeft = scrollWidth;
};
@ -218,22 +211,17 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
updateScrollLeft(currentScrollPosition);
const approxRangeLeft: number =
scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
if (currentScrollPosition >= approxRangeRight)
updateCurrentViewRenderPayload("right", currentView);
if (currentScrollPosition <= approxRangeLeft)
updateCurrentViewRenderPayload("left", currentView);
if (currentScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
if (currentScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
};
return (
<div
className={`${
fullScreenMode
? `fixed top-0 bottom-0 left-0 right-0 z-[999999] bg-custom-background-100`
: `relative`
fullScreenMode ? `fixed top-0 bottom-0 left-0 right-0 z-[999999] bg-custom-background-100` : `relative`
} ${
border ? `border border-custom-border-200` : ``
} flex h-full flex-col rounded-sm select-none bg-custom-background-100 shadow`}
@ -266,9 +254,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
<div
key={_chatView?.key}
className={`cursor-pointer rounded-sm p-1 px-2 text-xs ${
currentView === _chatView?.key
? `bg-custom-background-80`
: `hover:bg-custom-background-90`
currentView === _chatView?.key ? `bg-custom-background-80` : `hover:bg-custom-background-90`
}`}
onClick={() => handleChartView(_chatView?.key)}
>
@ -305,10 +291,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
bottomSpacing ? "mb-8" : ""
}`}
>
<div
id="gantt-sidebar"
className="h-full w-1/4 flex flex-col border-r border-custom-border-200"
>
<div id="gantt-sidebar" className="h-full w-1/4 flex flex-col border-r border-custom-border-200">
<div className="h-[60px] border-b border-custom-border-200 box-border flex-shrink-0 flex items-end justify-between gap-2 text-sm text-custom-text-300 font-medium pb-2 pl-10 pr-4">
<h6>{title}</h6>
<h6>Duration</h6>
@ -322,7 +305,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
/>
{chartBlocks && !(isCyclePage || isModulePage) && (
<div className="pl-2.5 py-3">
<GanttInlineCreateIssueForm
{/* <GanttInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
handleClose={() => setIsCreateIssueFormOpen(false)}
onSuccess={() => {
@ -344,7 +327,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/>
/> */}
{!isCreateIssueFormOpen && (
<button

View File

@ -98,7 +98,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
return (
<>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
<div className="w-full flex items-center flex-shrink-0 justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="relative w-full flex items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex gap-2 items-center">
{activeLayout === "spreadsheet" && <CheckCircle size={16} strokeWidth={2} />}
<span className="text-sm font-medium">Workspace {activeLayout === "spreadsheet" ? "Issues" : "Views"}</span>

View File

@ -1,11 +1,8 @@
export * from "./accept-issue-modal";
export * from "./decline-issue-modal";
export * from "./delete-issue-modal";
export * from "./modals";
export * from "./action-headers";
export * from "./filters-dropdown";
export * from "./filters-list";
export * from "./inbox-action-headers";
export * from "./inbox-issue-activity";
export * from "./inbox-issue-card";
export * from "./inbox-main-content";
export * from "./issue-activity";
export * from "./issue-card";
export * from "./issues-list-sidebar";
export * from "./select-duplicate";
export * from "./main-content";

View File

@ -0,0 +1,4 @@
export * from "./accept-issue-modal";
export * from "./decline-issue-modal";
export * from "./delete-issue-modal";
export * from "./select-duplicate";

View File

@ -1,13 +1,8 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// services

View File

@ -1,6 +0,0 @@
import React from "react";
export const IssueCalendarViewRoot = () => {
console.log();
return <div>IssueCalendarViewRoot</div>;
};

View File

@ -1,73 +0,0 @@
import React from "react";
// components
import { FilterPreviewHeader } from "./helpers/header";
import { FilterPreviewContent } from "./helpers/content";
import { FilterPreviewClear } from "./helpers/clear";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
export const MemberIcons = ({ display_name, avatar }: { display_name: string; avatar: string | null }) => (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[16px] h-[16px] flex justify-center items-center">
{avatar ? (
<img src={avatar} alt={display_name || ""} className="" />
) : (
<div className="text-xs w-full h-full flex justify-center items-center capitalize font-medium bg-gray-700 text-white">
{(display_name ?? "U")[0]}
</div>
)}
</div>
);
export const FilterAssignees = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null &&
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
};
const clearFilter = () => {
issueFilterStore.handleUserFilter("filters", "assignees", null);
};
return (
<>
{issueFilterStore?.userFilters?.filters?.assignees != null && (
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
<div className="flex-shrink-0">
<FilterPreviewHeader
title={`Assignees (${issueFilterStore?.userFilters?.filters?.assignees?.length || 0})`}
/>
</div>
<div className="relative flex items-center flex-wrap gap-2">
{issueFilterStore?.projectMembers &&
issueFilterStore?.projectMembers.length > 0 &&
issueFilterStore?.projectMembers.map(
(_member) =>
issueFilterStore?.userFilters?.filters?.assignees != null &&
issueFilterStore?.userFilters?.filters?.assignees.includes(_member?.member?.id) && (
<FilterPreviewContent
key={`assignees-${_member?.member?.id}`}
icon={<MemberIcons display_name={_member?.member.display_name} avatar={_member?.member.avatar} />}
title={`${_member?.member?.display_name}`}
onClick={() => handleFilter("assignees", _member?.member?.id)}
className="border border-custom-border-100 bg-custom-background-100"
/>
)
)}
<div className="flex-shrink-0">
<FilterPreviewClear onClick={clearFilter} />
</div>
</div>
</div>
)}
</>
);
});

View File

@ -1,63 +0,0 @@
import React from "react";
// components
import { MemberIcons } from "./assignees";
import { FilterPreviewHeader } from "./helpers/header";
import { FilterPreviewContent } from "./helpers/content";
import { FilterPreviewClear } from "./helpers/clear";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
export const FilterCreatedBy = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null &&
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
};
const clearFilter = () => {
issueFilterStore.handleUserFilter("filters", "created_by", null);
};
return (
<>
{issueFilterStore?.userFilters?.filters?.created_by != null && (
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
<div className="flex-shrink-0">
<FilterPreviewHeader
title={`Created By (${issueFilterStore?.userFilters?.filters?.created_by?.length || 0})`}
/>
</div>
<div className="relative flex items-center flex-wrap gap-2">
{issueFilterStore?.projectMembers &&
issueFilterStore?.projectMembers.length > 0 &&
issueFilterStore?.projectMembers.map(
(_member) =>
issueFilterStore?.userFilters?.filters?.created_by != null &&
issueFilterStore?.userFilters?.filters?.created_by.includes(_member?.member?.id) && (
<FilterPreviewContent
key={`create-by-${_member?.member?.id}`}
title={`${_member?.member?.display_name}`}
icon={<MemberIcons display_name={_member?.member.display_name} avatar={_member?.member.avatar} />}
onClick={() => handleFilter("created_by", _member?.member?.id)}
className="border border-custom-border-100 bg-custom-background-100"
/>
)
)}
<div className="flex-shrink-0">
<FilterPreviewClear onClick={clearFilter} />
</div>
</div>
</div>
)}
</>
);
});

View File

@ -1,17 +0,0 @@
// lucide icons
import { X } from "lucide-react";
interface IFilterPreviewClear {
onClick?: () => void;
}
export const FilterPreviewClear = ({ onClick }: IFilterPreviewClear) => (
<div
className="cursor-pointer"
onClick={() => {
if (onClick) onClick();
}}
>
<X width={12} strokeWidth={2} />
</div>
);

View File

@ -1,26 +0,0 @@
import { FilterPreviewClear } from "./clear";
interface IFilterPreviewContent {
icon?: React.ReactNode;
title?: string;
onClick?: () => void;
className?: string;
style?: any;
}
export const FilterPreviewContent = ({ icon, title, onClick, className, style }: IFilterPreviewContent) => (
<div
className={`flex-shrink-0 flex items-center gap-1.5 rounded-full px-[8px] transition-all ${className}`}
style={style ? style : {}}
>
<div className="flex-shrink-0">{icon}</div>
<div className="text-xs w-full whitespace-nowrap font-medium">{title}</div>
<div className="flex-shrink-0">
<FilterPreviewClear
onClick={() => {
if (onClick) onClick();
}}
/>
</div>
</div>
);

View File

@ -1,12 +0,0 @@
interface IFilterPreviewHeader {
title: string;
}
export const FilterPreviewHeader = ({ title }: IFilterPreviewHeader) => {
console.log();
return (
<div className="flex items-center justify-between gap-2">
<div className="text-gray-500 text-xs text-custom-text-300 font-medium">{title}</div>
</div>
);
};

View File

@ -1,69 +0,0 @@
import React from "react";
// components
import { FilterPriority } from "./priority";
import { FilterState } from "./state";
import { FilterStateGroup } from "./state-group";
import { FilterAssignees } from "./assignees";
import { FilterCreatedBy } from "./created-by";
import { FilterLabels } from "./labels";
import { FilterStartDate } from "./start-date";
import { FilterTargetDate } from "./target-date";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// default data
// import { issueFilterVisibilityData } from "store/helpers/issue-data";
export const FilterPreview = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const handleFilterSectionVisibility = (section_key: string) => {
// issueFilterStore?.issueView &&
// issueFilterStore?.issueLayout &&
// issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"]?.filters?.[
// issueFilterStore?.issueLayout
// ]?.includes(section_key);
};
const validateFiltersAvailability =
issueFilterStore?.userFilters?.filters != null &&
Object.keys(issueFilterStore?.userFilters?.filters).length > 0 &&
Object.keys(issueFilterStore?.userFilters?.filters)
.map((key) => issueFilterStore?.userFilters?.filters?.[key]?.length)
.filter((v) => v != undefined || v != null).length > 0;
return (
<>
{validateFiltersAvailability && (
<div className="w-full h-full overflow-hidden overflow-y-auto relative max-h-[500px] flex flex-wrap p-2 border-b border-custom-border-80 shadow-sm">
{/* priority */}
{handleFilterSectionVisibility("priority") && <FilterPriority />}
{/* state group */}
{handleFilterSectionVisibility("state_group") && <FilterStateGroup />}
{/* state */}
{handleFilterSectionVisibility("state") && <FilterState />}
{/* assignees */}
{handleFilterSectionVisibility("assignees") && <FilterAssignees />}
{/* created_by */}
{handleFilterSectionVisibility("created_by") && <FilterCreatedBy />}
{/* labels */}
{handleFilterSectionVisibility("labels") && <FilterLabels />}
{/* start_date */}
{handleFilterSectionVisibility("start_date") && <FilterStartDate />}
{/* due_date */}
{handleFilterSectionVisibility("due_date") && <FilterTargetDate />}
</div>
)}
</>
);
});

View File

@ -1,73 +0,0 @@
import React from "react";
// components
import { FilterPreviewHeader } from "./helpers/header";
import { FilterPreviewContent } from "./helpers/content";
import { FilterPreviewClear } from "./helpers/clear";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
const LabelIcons = ({ color }: { color: string }) => (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
<div className={`w-[12px] h-[12px] rounded-full`} style={{ backgroundColor: color }} />
</div>
);
export const FilterLabels = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const stateStyles = (color: any) => ({ color: color, backgroundColor: `${color}20` });
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null &&
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
};
const clearFilter = () => {
issueFilterStore.handleUserFilter("filters", "labels", null);
};
const handleLabels =
issueFilterStore.issueView && issueFilterStore.issueView === "my_issues"
? issueFilterStore?.workspaceLabels
: issueFilterStore?.projectLabels;
return (
<>
{issueFilterStore?.userFilters?.filters?.labels != null && (
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
<div className="flex-shrink-0">
<FilterPreviewHeader title={`Labels (${issueFilterStore?.userFilters?.filters?.labels?.length || 0})`} />
</div>
<div className="relative flex items-center flex-wrap gap-2">
{handleLabels &&
handleLabels.length > 0 &&
handleLabels.map(
(_label) =>
issueFilterStore?.userFilters?.filters?.labels != null &&
issueFilterStore?.userFilters?.filters?.labels.includes(_label?.id) && (
<FilterPreviewContent
key={_label?.id}
onClick={() => handleFilter("labels", _label?.id)}
icon={<LabelIcons color={_label.color} />}
title={_label.name}
style={stateStyles(_label.color)}
/>
)
)}
<div className="flex-shrink-0">
<FilterPreviewClear onClick={clearFilter} />
</div>
</div>
</div>
)}
</>
);
});

View File

@ -1,79 +0,0 @@
import React from "react";
// lucide icons
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
// components
import { FilterPreviewHeader } from "./helpers/header";
import { FilterPreviewContent } from "./helpers/content";
import { FilterPreviewClear } from "./helpers/clear";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
const PriorityIcons = ({ priority }: { priority: string }) => {
if (priority === "urgent") return <AlertCircle size={12} strokeWidth={2} />;
if (priority === "high") return <SignalHigh size={12} strokeWidth={4} />;
if (priority === "medium") return <SignalMedium size={12} strokeWidth={4} />;
if (priority === "low") return <SignalLow size={12} strokeWidth={4} />;
return <Ban size={12} strokeWidth={2} />;
};
const classNamesStyling = (priority: string) => {
if (priority == "urgent") return "bg-red-500/20 text-red-500";
if (priority == "high") return "bg-orange-500/20 text-orange-500 !-pt-[30px]";
if (priority == "medium") return "bg-orange-500/20 text-orange-500 -pt-2";
if (priority == "low") return "bg-green-500/20 text-green-500 -pt-2";
return "bg-gray-500/10 text-gray-500";
};
export const FilterPriority = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null &&
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
};
const clearFilter = () => {
issueFilterStore.handleUserFilter("filters", "priority", null);
};
return (
<>
{issueFilterStore?.userFilters?.filters?.priority != null && (
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
<div className="flex-shrink-0">
<FilterPreviewHeader
title={`Priority (${issueFilterStore?.userFilters?.filters?.priority?.length || 0})`}
/>
</div>
<div className="relative flex items-center flex-wrap gap-2">
{issueFilterStore?.issueRenderFilters?.priority &&
issueFilterStore?.issueRenderFilters?.priority.length > 0 &&
issueFilterStore?.issueRenderFilters?.priority.map(
(_priority) =>
issueFilterStore?.userFilters?.filters?.priority != null &&
issueFilterStore?.userFilters?.filters?.priority.includes(_priority?.key) && (
<FilterPreviewContent
key={_priority?.key}
icon={<PriorityIcons priority={_priority.key} />}
title={_priority.title}
className={classNamesStyling(_priority?.key)}
onClick={() => handleFilter("priority", _priority?.key)}
/>
)
)}
<div className="flex-shrink-0">
<FilterPreviewClear onClick={clearFilter} />
</div>
</div>
</div>
)}
</>
);
});

View File

@ -1,56 +0,0 @@
import React from "react";
// components
import { FilterPreviewHeader } from "./helpers/header";
import { FilterPreviewContent } from "./helpers/content";
import { FilterPreviewClear } from "./helpers/clear";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
export const FilterStartDate = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null &&
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
};
const clearFilter = () => {
issueFilterStore.handleUserFilter("filters", "start_date", null);
};
return (
<>
{issueFilterStore?.userFilters?.filters?.start_date != null && (
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
<div className="flex-shrink-0">
<FilterPreviewHeader title={`Start Date`} />
</div>
<div className="relative flex items-center flex-wrap gap-2">
{issueFilterStore?.issueRenderFilters?.start_date &&
issueFilterStore?.issueRenderFilters?.start_date.length > 0 &&
issueFilterStore?.issueRenderFilters?.start_date.map((_startDate) => (
<FilterPreviewContent
key={_startDate?.key}
title={_startDate.title}
className="border border-custom-border-100 bg-custom-background-100"
onClick={() => handleFilter("start_date", _startDate?.key)}
/>
))}
</div>
<div className="flex-shrink-0">
<FilterPreviewClear onClick={clearFilter} />
</div>
</div>
)}
</>
);
});

View File

@ -1,129 +0,0 @@
import React from "react";
import {
StateGroupBacklogIcon,
StateGroupCancelledIcon,
StateGroupCompletedIcon,
StateGroupStartedIcon,
StateGroupUnstartedIcon,
} from "components/icons";
// components
import { FilterPreviewHeader } from "./helpers/header";
import { FilterPreviewContent } from "./helpers/content";
import { FilterPreviewClear } from "./helpers/clear";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
export const StateGroupIcons = ({ stateGroup, color = null }: { stateGroup: string; color?: string | null }) => {
if (stateGroup === "cancelled")
return (
<StateGroupCancelledIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
);
if (stateGroup === "completed")
return (
<StateGroupCompletedIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
);
if (stateGroup === "started")
return (
<StateGroupStartedIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
);
if (stateGroup === "unstarted")
return (
<StateGroupUnstartedIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
);
if (stateGroup === "backlog")
return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
<StateGroupBacklogIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
</div>
);
return <></>;
};
export const stateStyles = (stateGroup: string, color: any) => {
if (stateGroup === "cancelled") {
return {
color: color ? color : STATE_GROUP_COLORS[stateGroup],
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
};
}
if (stateGroup === "completed") {
return {
color: color ? color : STATE_GROUP_COLORS[stateGroup],
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
};
}
if (stateGroup === "started") {
return {
color: color ? color : STATE_GROUP_COLORS[stateGroup],
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
};
}
if (stateGroup === "unstarted") {
return {
color: color ? color : STATE_GROUP_COLORS[stateGroup],
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
};
}
if (stateGroup === "backlog") {
return {
color: color ? color : STATE_GROUP_COLORS[stateGroup],
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
};
}
};
export const FilterStateGroup = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null &&
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
};
const clearFilter = () => {
issueFilterStore.handleUserFilter("filters", "state_group", null);
};
return (
<>
{issueFilterStore?.userFilters?.filters?.state_group != null && (
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
<div className="flex-shrink-0">
<FilterPreviewHeader
title={`State Group (${issueFilterStore?.userFilters?.filters?.state_group?.length || 0})`}
/>
</div>
<div className="relative flex items-center flex-wrap gap-2">
{issueFilterStore?.issueRenderFilters?.state_group &&
issueFilterStore?.issueRenderFilters?.state_group.length > 0 &&
issueFilterStore?.issueRenderFilters?.state_group.map(
(_stateGroup) =>
issueFilterStore?.userFilters?.filters?.state_group != null &&
issueFilterStore?.userFilters?.filters?.state_group.includes(_stateGroup?.key) && (
<FilterPreviewContent
key={_stateGroup?.key}
icon={<StateGroupIcons stateGroup={_stateGroup.key} />}
title={_stateGroup.title}
style={stateStyles(_stateGroup?.key, null)}
onClick={() => handleFilter("state_group", _stateGroup?.key)}
/>
)
)}
<div className="flex-shrink-0">
<FilterPreviewClear onClick={clearFilter} />
</div>
</div>
</div>
)}
</>
);
});

View File

@ -1,66 +0,0 @@
import React from "react";
// components
import { StateGroupIcons, stateStyles } from "./state-group";
import { FilterPreviewHeader } from "./helpers/header";
import { FilterPreviewContent } from "./helpers/content";
import { FilterPreviewClear } from "./helpers/clear";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// store default data
import { stateGroups } from "store/helpers/issue-data";
export const FilterState = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null &&
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
};
const clearFilter = () => {
issueFilterStore.handleUserFilter("filters", "state", null);
};
return (
<>
{issueFilterStore?.userFilters?.filters?.state != null && (
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
<div className="flex-shrink-0">
<FilterPreviewHeader title={`State (${issueFilterStore?.userFilters?.filters?.state?.length || 0})`} />
</div>
<div className="relative flex items-center flex-wrap gap-2">
{stateGroups.map(
(_stateGroup) =>
issueFilterStore?.projectStates &&
issueFilterStore?.projectStates[_stateGroup?.key] &&
issueFilterStore?.projectStates[_stateGroup?.key].length > 0 &&
issueFilterStore?.projectStates[_stateGroup?.key].map(
(_state: any) =>
issueFilterStore?.userFilters?.filters?.state != null &&
issueFilterStore?.userFilters?.filters?.state.includes(_state?.id) && (
<FilterPreviewContent
key={_state?.id}
icon={<StateGroupIcons stateGroup={_stateGroup?.key} color={_state?.color} />}
title={_state?.name}
style={stateStyles(_state?.group, _state?.color)}
onClick={() => handleFilter("state", _state?.id)}
/>
)
)
)}
<div className="flex-shrink-0">
<FilterPreviewClear onClick={clearFilter} />
</div>
</div>
</div>
)}
</>
);
});

View File

@ -1,56 +0,0 @@
import React from "react";
// components
import { FilterPreviewHeader } from "./helpers/header";
import { FilterPreviewContent } from "./helpers/content";
import { FilterPreviewClear } from "./helpers/clear";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
export const FilterTargetDate = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null &&
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
};
const clearFilter = () => {
issueFilterStore.handleUserFilter("filters", "target_date", null);
};
return (
<>
{issueFilterStore?.userFilters?.filters?.target_date != null && (
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
<div className="flex-shrink-0">
<FilterPreviewHeader title={`Target Date`} />
</div>
<div className="relative flex items-center flex-wrap gap-2">
{issueFilterStore?.issueRenderFilters?.due_date &&
issueFilterStore?.issueRenderFilters?.due_date.length > 0 &&
issueFilterStore?.issueRenderFilters?.due_date.map((_targetDate) => (
<FilterPreviewContent
key={_targetDate?.key}
title={_targetDate.title}
className="border border-custom-border-100 bg-custom-background-100"
onClick={() => handleFilter("target_date", _targetDate?.key)}
/>
))}
</div>
<div className="flex-shrink-0">
<FilterPreviewClear onClick={clearFilter} />
</div>
</div>
)}
</>
);
});

View File

@ -1,6 +0,0 @@
import React from "react";
export const IssueGanttViewRoot = () => {
console.log();
return <div>IssueGanttViewRoot</div>;
};

View File

@ -1,45 +0,0 @@
// react beautiful dnd
import { Draggable } from "react-beautiful-dnd";
interface IssueContentProps {
columnId: string;
issues: any;
}
export const IssueContent = ({ columnId, issues }: IssueContentProps) => {
console.log();
return (
<>
{issues && issues.length > 0 ? (
<>
{issues.map((issue: any, index: any) => (
<Draggable draggableId={issue.id} index={index} key={`issue-blocks-${columnId}-${issue.id}`}>
{(provided: any, snapshot: any) => (
<div
key={issue.id}
className="p-1.5 hover:cursor-default"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
<div
className={`min-h-[106px] text-sm rounded p-2 px-3 shadow-md space-y-[4px] border transition-all hover:cursor-grab ${
snapshot.isDragging ? `border-blue-500 bg-blue-50` : `border-transparent bg-white`
}`}
>
<div className="text-xs line-clamp-1 text-gray-500">ONE-{issue.sequence_id}</div>
<div className="line-clamp-2 h-[40px] text-sm font-medium">{issue.name}</div>
<div className="h-[22px]">Footer</div>
</div>
</div>
)}
</Draggable>
))}
</>
) : (
<div>No issues are available.</div>
)}
</>
);
};

View File

@ -1,18 +0,0 @@
// lucide icons
import { Plus } from "lucide-react";
export const IssueHeader = () => (
<div className="relative flex items-center w-full h-full gap-1">
{/* default layout */}
<div className="flex-shrink-0 w-[24px] h-[24px] flex justify-center items-center">I</div>
<div className="line-clamp-1 font-medium">Kanban Issue Heading</div>
<div className="flex-shrink-0 w-[24px] h-[24px] flex justify-center items-center">0</div>
<div className="ml-auto flex-shrink-0 w-[24px] h-[24px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-gray-100 transition-all">
M
</div>
<div className="flex-shrink-0 w-[24px] h-[24px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-gray-100 transition-all text-blue-800">
<Plus size={16} strokeWidth={2} />
</div>
</div>
);

View File

@ -1,75 +0,0 @@
import React from "react";
// react beautiful dnd
import { DragDropContext, Droppable } from "react-beautiful-dnd";
// components
import { IssueHeader } from "./header";
import { IssueContent } from "./content";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
export const IssueKanBanViewRoot = observer(() => {
const { issueView: issueViewStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
const onDragEnd = (result: any) => {
if (!result) return;
if (
result.destination &&
result.source &&
result.destination.droppableId === result.source.droppableId &&
result.destination.index === result.source.index
)
return;
issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
};
return (
<div className="relative w-full h-full">
{issueViewStore.loader || issueViewStore?.getIssues === null ? (
<div>Loading...</div>
) : (
<>
{issueViewStore?.getIssues && Object.keys(issueViewStore?.getIssues).length > 0 ? (
<DragDropContext onDragEnd={onDragEnd}>
<div className="relative w-full h-full overflow-hidden !overflow-x-scroll flex">
{Object.keys(issueViewStore?.getIssues).map((_issueStateKey: any) => (
<div key={`${_issueStateKey}`} className="flex-shrink-0 w-[380px] h-full relative flex flex-col">
<div className="flex-shrink-0 w-full p-2">
<IssueHeader />
</div>
<Droppable droppableId={`${_issueStateKey}`}>
{(provided: any, snapshot: any) => (
<div
className={`w-full h-full relative transition-all py-1.5 overflow-hidden overflow-y-auto ${
snapshot.isDraggingOver ? `bg-orange-50` : `bg-gray-50`
}`}
{...provided.droppableProps}
ref={provided.innerRef}
>
{issueViewStore?.getIssues && (
<IssueContent
columnId={_issueStateKey}
issues={issueViewStore?.getIssues[_issueStateKey]}
/>
)}
{provided.placeholder}
</div>
)}
</Droppable>
</div>
))}
</div>
</DragDropContext>
) : (
<div>No Issues are available</div>
)}
</>
)}
</div>
);
});

View File

@ -1,31 +0,0 @@
import { FC } from "react";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
export interface IIssueListGroupHeader {
groupId: string;
groupBy: string;
}
export const IssueListGroupHeader: FC<IIssueListGroupHeader> = (props) => {
const { groupId, groupBy } = props;
const { issueFilters: issueFilterStore }: RootStore = useMobxStore();
return (
<div>
{groupBy === "state" && <>{issueFilterStore.getProjectStateById(groupId)?.name}</>}
{groupBy === "state_detail.group" && <>{groupId}</>}
{groupBy === "priority" && <>{groupId}</>}
{groupBy === "project" && <>{issueFilterStore.workspaceProjects?.find((p) => (p.id = groupId))}</>}
{groupBy === "labels" && <>{issueFilterStore.projectLabels?.find((p) => p.id === groupId)?.name || " None"}</>}
{groupBy === "assignees" && (
<>{issueFilterStore.projectMembers?.find((p) => p?.member?.id === groupId)?.member?.display_name || " None"}</>
)}
{groupBy === "created_by" && (
<>{issueFilterStore.projectMembers?.find((p) => p?.member?.id === groupId)?.member?.display_name || " None"}</>
)}
</div>
);
};

View File

@ -1,4 +0,0 @@
export * from "./root";
export * from "./list";
export * from "./item";
export * from "./group-header";

View File

@ -1,246 +0,0 @@
import React, { FC, useState } from "react";
import { observer } from "mobx-react-lite";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
import { IIssue } from "types";
import useUserAuth from "hooks/use-user-auth";
// icons
import {
ClipboardDocumentCheckIcon,
LinkIcon,
PencilIcon,
TrashIcon,
XMarkIcon,
ArrowTopRightOnSquareIcon,
PaperClipIcon,
} from "@heroicons/react/24/outline";
// components
import { CustomMenu, ContextMenu } from "components/ui";
import { Tooltip } from "@plane/ui";
import { LayerDiagonalIcon } from "components/icons";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
ViewIssueLabel,
ViewPrioritySelect,
ViewStartDateSelect,
ViewStateSelect,
} from "components/issues";
import { IssuePrioritySelect } from "../properties";
export interface IIssueListItem {
issue: IIssue;
groupId: string;
}
export const IssueListItem: FC<IIssueListItem> = observer((props) => {
const { issue, groupId } = props;
// store
const { user: userStore, issueFilters: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore();
const displayProperties = issueFilterStore.userFilters?.display_properties;
const workspaceId = issueFilterStore.workspaceId;
const projectId = issueFilterStore.projectId;
const issueId = issue.id;
const user = userStore.currentUser;
console.log("userStore", userStore);
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const isNotAllowed = false;
// const isNotAllowed =
// userAuth?.isGuest || userAuth?.isViewer || disableUserActions || isArchivedIssues;
const partialUpdateIssue = (data: any) => {
// console.log("data", data);
if (workspaceId && projectId && issueId) issueDetailStore.updateIssueAsync(workspaceId, projectId, issueId, data);
};
return (
<div>
<>
<ContextMenu
clickEvent={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
>
{/* {!isNotAllowed && (
<>
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
Edit issue
</ContextMenu.Item>
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
Delete issue
</ContextMenu.Item>
</>
)}
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
<a href={issuePath} target="_blank" rel="noreferrer noopener">
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
</a> */}
</ContextMenu>
<div
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition(e);
}}
>
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
<div className="group relative flex items-center gap-2">
{/* {properties.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)} */}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<button
type="button"
className="truncate text-[0.825rem] text-custom-text-100"
onClick={() => {
// if (!isDraftIssues) openPeekOverview(issue);
// if (handleDraftIssueSelect) handleDraftIssueSelect(issue);
}}
>
{issue.name}
</button>
</Tooltip>
</div>
</div>
<div className={`flex flex-shrink-0 items-center gap-2 text-xs `}>
{displayProperties?.priority && (
// <ViewPrioritySelect
// issue={issue}
// partialUpdateIssue={partialUpdateIssue}
// position="right"
// user={user as any}
// isNotAllowed={isNotAllowed}
// />
<IssuePrioritySelect issue={issue} groupId={groupId} />
)}
{displayProperties?.state && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
user={user as any}
isNotAllowed={isNotAllowed}
/>
)}
{displayProperties?.start_date && issue.start_date && (
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user as any}
isNotAllowed={isNotAllowed}
/>
)}
{displayProperties?.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user as any}
isNotAllowed={isNotAllowed}
/>
)}
{displayProperties?.labels && <ViewIssueLabel labelDetails={issue.label_details} maxRender={3} />}
{displayProperties?.assignee && (
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
user={user as any}
isNotAllowed={isNotAllowed}
/>
)}
{displayProperties?.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
user={user as any}
isNotAllowed={isNotAllowed}
/>
)}
{displayProperties?.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LayerDiagonalIcon className="h-3.5 w-3.5" />
{issue.sub_issues_count}
</div>
</Tooltip>
</div>
)}
{displayProperties?.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{displayProperties?.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
{/* {type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<div className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove from {type}</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)} */}
</div>
</div>
</>
</div>
);
});

View File

@ -1,14 +0,0 @@
import React, { FC } from "react";
import { IIssue } from "types";
import { IssueListItem } from "./item";
import { observer } from "mobx-react-lite";
export interface IIssueListView {
issues: IIssue[] | null;
groupId: string;
}
export const IssueListView: FC<IIssueListView> = observer((props) => {
const { issues = [], groupId } = props;
return <div>{issues && issues.map((issue) => <IssueListItem issue={issue} groupId={groupId} />)}</div>;
});

View File

@ -1,47 +0,0 @@
import React from "react";
import { Disclosure } from "@headlessui/react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// components
import { IssueListView } from "./list";
import { IssueListGroupHeader } from "./group-header";
export const IssueListViewRoot = observer(() => {
const { issueView: issueViewStore, issueFilters: issueFilterStore }: RootStore = useMobxStore();
console.log("issueViewStore", issueViewStore);
console.log("userFilters", issueFilterStore.userFilters);
console.log("issueFilterStore", issueFilterStore);
return (
<div className="relative w-full h-full">
{issueViewStore.loader || issueViewStore?.getIssues === null ? (
<div>Loading...</div>
) : (
<>
{Object.keys(issueViewStore?.getIssues).map((groupId: any) => (
<Disclosure key={groupId}>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75">
<IssueListGroupHeader
groupId={groupId}
groupBy={issueFilterStore.userFilters?.display_filters["group_by"] || ""}
/>
</Disclosure.Button>
<Disclosure.Panel className="px-4 pt-4 pb-2">
<IssueListView
issues={(groupId && issueViewStore?.getIssues?.[groupId]) ?? null}
groupId={groupId}
/>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</>
)}
</div>
);
});

View File

@ -1 +0,0 @@
export * from "./priority-select";

View File

@ -1,62 +0,0 @@
import { FC, Fragment, useState } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { useMobxStore } from "lib/mobx/store-provider";
import { IIssue } from "types";
import { observer } from "mobx-react-lite";
export interface IIssuePrioritySelect {
issue: IIssue;
groupId: string;
}
export const IssuePrioritySelect: FC<IIssuePrioritySelect> = observer((props) => {
const { issue, groupId } = props;
const { issueView: issueViewStore, issueFilters: issueFilterStore } = useMobxStore();
const priorityList = issueFilterStore.issueRenderFilters.priority;
const selected = priorityList.find((p) => p.key === issue.priority);
const changePriority = (selectedPriority: any) => {
issueViewStore.updateIssues(groupId, issue.id, { priority: selectedPriority.key });
};
return (
<Listbox value={selected} onChange={changePriority}>
<div className="relative mt-1">
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-3 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm">
<span className="block truncate text-xs">{selected?.title || "None"}</span>
</Listbox.Button>
<Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
<Listbox.Options className="absolute mt-1 max-h-60 w-[200px] overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm z-10">
{priorityList.map((priority) => (
<Listbox.Option
key={priority.key}
className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${
active ? "bg-amber-100 text-amber-900" : "text-gray-900"
}`
}
value={priority}
>
{({ selected }) => (
<>
<span className={`block truncate ${selected ? "font-medium" : "font-normal"}`}>
{priority.title}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
);
});

View File

@ -1,89 +0,0 @@
import React from "react";
// components
import { LayoutSelection } from "../issues/issue-layouts/filters/header/layout-selection";
import { IssueDropdown } from "../issues/issue-layouts/filters/header/helpers/dropdown";
import { FilterSelection } from "../issues/issue-layouts/filters/header/filters/filters-selection";
import { DisplayFiltersSelection } from "../issues/issue-layouts/filters/header/display-filters";
import { FilterPreview } from "./filters-preview";
import { IssueListViewRoot } from "./list/root";
import { IssueKanBanViewRoot } from "./kanban";
import { IssueCalendarViewRoot } from "./calendar";
import { IssueSpreadsheetViewRoot } from "./spreadsheet";
import { IssueGanttViewRoot } from "./gantt";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
export const IssuesRoot = observer(() => {
const {
workspace: workspaceStore,
project: projectStore,
issue: issueStore,
issueFilter: issueFilterStore,
}: RootStore = useMobxStore();
// console.log("---");
// console.log("--- workspace store");
// console.log("workspaces", workspaceStore?.workspaces);
// console.log("workspace id", workspaceStore?.workspaceId);
// console.log("current workspace", workspaceStore?.currentWorkspace);
// console.log("workspace by id", workspaceStore?.workspaceById("plane"));
// console.log("workspace labels", workspaceStore?.workspaceLabels);
// console.log("workspace label by id", workspaceStore?.workspaceLabelById("1fe1031b-8986-4e6a-86cc-0d2fe3ac272f"));
// console.log("--- project store");
// console.log("workspace projects", projectStore?.workspaceProjects);
// console.log("project id", projectStore?.projectId);
// console.log("project state by groups", projectStore?.projectStatesByGroups);
// console.log("project states", projectStore?.projectStates);
// console.log("project labels", projectStore?.projectLabels);
// console.log("project members", projectStore?.projectMembers);
// projectStore?.projectStates &&
// console.log("project state by id", projectStore?.projectStateById(projectStore?.projectStates?.[0]?.id));
// projectStore?.projectLabels &&
// console.log("project label by id", projectStore?.projectLabelById(projectStore?.projectLabels?.[0]?.id));
// projectStore?.projectMembers &&
// console.log("project member by id", projectStore?.projectMemberById(projectStore?.projectMembers?.[0]?.id));
// console.log("--- issue filter store");
// console.log("issues filters", issueFilterStore?.issueFilters);
// console.log("--- issue store");
// console.log("issues", issueStore?.issues);
// console.log("---");
return (
<div className="w-full h-full relative flex flex-col overflow-hidden">
<div className="flex-shrink-0 h-[60px] border-b border-custom-border-80 shadow-sm">
<div className="w-full h-full p-2 px-5 relative flex justify-between items-center gap-2">
<div>
<div>Filter Header</div>
</div>
<div className="relative flex items-center gap-2">
{/* <IssueDropdown title={"Filters"}>
<FilterSelection />
</IssueDropdown>
<IssueDropdown title={"View"}>
<DisplayFiltersSelection />
</IssueDropdown>
<LayoutSelection /> */}
</div>
</div>
</div>
<div className="flex-shrink-0">
<FilterPreview />
</div>
<div className="w-full h-full relative overflow-hidden">
{issueFilterStore?.userDisplayFilters.layout === "list" && <IssueListViewRoot />}
{issueFilterStore?.userDisplayFilters.layout === "kanban" && <IssueKanBanViewRoot />}
{issueFilterStore?.userDisplayFilters.layout === "calendar" && <IssueCalendarViewRoot />}
{issueFilterStore?.userDisplayFilters.layout === "spreadsheet" && <IssueSpreadsheetViewRoot />}
{issueFilterStore?.userDisplayFilters.layout === "gantt_chart" && <IssueGanttViewRoot />}
</div>
</div>
);
});

View File

@ -1,6 +0,0 @@
import React from "react";
export const IssueSpreadsheetViewRoot = () => {
console.log();
return <div>IssueSpreadsheetViewRoot</div>;
};

View File

@ -1,12 +1,9 @@
export * from "./dropdowns";
export * from "./roots";
export * from "./calendar";
export * from "./cycle-root";
export * from "./types.d";
export * from "./day-tile";
export * from "./header";
export * from "./issue-blocks";
export * from "./module-root";
export * from "./project-view-root";
export * from "./root";
export * from "./week-days";
export * from "./week-header";

View File

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

View File

@ -1,13 +1,9 @@
export * from "./cycle-root";
export * from "./roots";
export * from "./date";
export * from "./filters-list";
export * from "./global-views-root";
export * from "./label";
export * from "./members";
export * from "./module-root";
export * from "./priority";
export * from "./project-view-root";
export * from "./project";
export * from "./root";
export * from "./state";
export * from "./state-group";

View File

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

View File

@ -8,7 +8,7 @@ import { AppliedFiltersList } from "components/issues";
// types
import { IIssueFilterOptions } from "types";
export const AppliedFiltersRoot: React.FC = observer(() => {
export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

View File

@ -87,6 +87,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
order_by: val,
})
}
orderByOptions={layoutDisplayFiltersOptions?.display_filters.order_by ?? []}
/>
</div>
)}

View File

@ -11,10 +11,11 @@ import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue";
type Props = {
selectedOrderBy: TIssueOrderByOptions | undefined;
handleUpdate: (val: TIssueOrderByOptions) => void;
orderByOptions: TIssueOrderByOptions[];
};
export const FilterOrderBy: React.FC<Props> = observer((props) => {
const { selectedOrderBy, handleUpdate } = props;
const { selectedOrderBy, handleUpdate, orderByOptions } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
@ -27,7 +28,7 @@ export const FilterOrderBy: React.FC<Props> = observer((props) => {
/>
{previewEnabled && (
<div>
{ISSUE_ORDER_BY_OPTIONS.map((orderBy) => (
{ISSUE_ORDER_BY_OPTIONS.filter((option) => orderByOptions.includes(option.key)).map((orderBy) => (
<FilterOption
key={orderBy?.key}
isChecked={selectedOrderBy === orderBy?.key ? true : false}

View File

@ -23,7 +23,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
});
return (
<Popover>
<Popover as="div">
{({ open }) => {
if (open) {
}

View File

@ -8,14 +8,4 @@ export * from "./gantt";
export * from "./kanban";
export * from "./spreadsheet";
// global view layout
export * from "./global-view-all-layouts";
// cycle root layout
export * from "./cycle-layout-root";
// module root layout
export * from "./module-all-layouts";
// project view layout
export * from "./project-view-all-layouts";
export * from "./roots";

View File

@ -52,9 +52,9 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const activeLayout = issueFilterStore.userDisplayFilters.layout;
return (
<div className="relative w-full h-full flex flex-col overflow-auto">
<div className="relative w-full h-full flex flex-col overflow-hidden">
<CycleAppliedFiltersRoot />
<div className="w-full h-full">
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (

View File

@ -17,7 +17,7 @@ type Props = {
type?: TStaticViewTypes;
};
export const GlobalViewsAllLayouts: React.FC<Props> = observer((props) => {
export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
const { type } = props;
const router = useRouter();
@ -70,9 +70,9 @@ export const GlobalViewsAllLayouts: React.FC<Props> = observer((props) => {
: undefined;
return (
<div className="relative w-full h-full flex flex-col overflow-auto">
<div className="relative w-full h-full flex flex-col overflow-hidden">
<GlobalViewsAppliedFiltersRoot />
<div className="h-full w-full">
<div className="h-full w-full overflow-auto">
<SpreadsheetView
displayProperties={workspaceFilterStore.workspaceDisplayProperties}
displayFilters={workspaceFilterStore.workspaceDisplayFilters}

View File

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

View File

@ -15,7 +15,7 @@ import {
ModuleSpreadsheetLayout,
} from "components/issues";
export const ModuleAllLayouts: React.FC = observer(() => {
export const ModuleLayoutRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
@ -50,9 +50,9 @@ export const ModuleAllLayouts: React.FC = observer(() => {
const activeLayout = issueFilterStore.userDisplayFilters.layout;
return (
<div className="relative w-full h-full flex flex-col overflow-auto">
<div className="relative w-full h-full flex flex-col overflow-hidden">
<ModuleAppliedFiltersRoot />
<div className="h-full w-full">
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (

View File

@ -7,15 +7,15 @@ import useSWR from "swr";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import {
AppliedFiltersRoot,
ListLayout,
CalendarLayout,
GanttLayout,
KanBanLayout,
ProjectAppliedFiltersRoot,
SpreadsheetLayout,
} from "components/issues";
export const AllViews: React.FC = observer(() => {
export const ProjectLayoutRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query as {
workspaceSlug: string;
@ -46,9 +46,9 @@ export const AllViews: React.FC = observer(() => {
const activeLayout = issueFilterStore.userDisplayFilters.layout;
return (
<div className="relative w-full h-full flex flex-col overflow-auto">
<AppliedFiltersRoot />
<div className="w-full h-full">
<div className="relative w-full h-full flex flex-col overflow-hidden">
<ProjectAppliedFiltersRoot />
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (

View File

@ -15,7 +15,7 @@ import {
ProjectViewSpreadsheetLayout,
} from "components/issues";
export const ProjectViewAllLayouts: React.FC = observer(() => {
export const ProjectViewLayoutRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query;
@ -52,9 +52,9 @@ export const ProjectViewAllLayouts: React.FC = observer(() => {
const activeLayout = issueFilterStore.userDisplayFilters.layout;
return (
<div className="relative h-full w-full flex flex-col overflow-auto">
<div className="relative h-full w-full flex flex-col overflow-hidden">
<ProjectViewAppliedFiltersRoot />
<div className="h-full w-full">
<div className="h-full w-full overflow-y-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (

View File

@ -1,3 +1,2 @@
export * from "./blocks";
export * from "./module-issues-layout";
export * from "./modules-list-layout";

View File

@ -1,51 +0,0 @@
import { useRouter } from "next/router";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user";
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
// types
import { IIssue } from "types";
type Props = { disableUserActions: boolean };
export const ModuleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
const { displayFilters } = useIssuesView();
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={null}
blockUpdateHandler={(block, payload) => {}}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
bottomSpacing
/>
</div>
</>
);
};

View File

@ -1,165 +0,0 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Transition, Combobox } from "@headlessui/react";
// services
import workspaceService from "services/workspace.service";
// types
import type { Props } from "./types";
// fetch-keys
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
const SearchListbox: React.FC<Props> = ({
title,
options,
onChange,
value,
multiple: canSelectMultiple,
icon,
optionsFontsize,
buttonClassName,
optionsClassName,
assignee = false,
}) => {
const [query, setQuery] = useState("");
const router = useRouter();
const { workspaceSlug } = router.query;
const filteredOptions =
query === ""
? options
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
const props: any = {
value,
onChange,
};
if (canSelectMultiple) {
props.value = props.value ?? [];
props.onChange = (value: string[]) => {
onChange(value);
};
props.multiple = true;
}
const { data: people } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const userAvatar = (userId: string) => {
const user = people?.find((p) => p.member.id === userId);
if (!user) return;
if (user.member.avatar && user.member.avatar !== "") {
return (
<div className="relative h-4 w-4">
<img
src={user.member.avatar}
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
alt="avatar"
/>
</div>
);
} else
return (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{user.member.display_name.charAt(0)}
</div>
);
};
return (
<Combobox as="div" {...props} className="relative flex-shrink-0">
{({ open }: any) => (
<>
<Combobox.Label className="sr-only">{title}</Combobox.Label>
<Combobox.Button
className={`flex cursor-pointer items-center gap-1 rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm duration-300 hover:bg-custom-background-90 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary ${
buttonClassName || ""
}`}
>
{icon ?? null}
<span
className={`hidden truncate sm:block ${
value === null || value === undefined ? "" : "text-custom-text-100"
}`}
>
{Array.isArray(value)
? value
.map((v) => options?.find((option) => option.value === v)?.display)
.join(", ") || title
: options?.find((option) => option.value === value)?.display || title}
</span>
</Combobox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-custom-background-80 py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
optionsFontsize === "sm"
? "text-xs"
: optionsFontsize === "md"
? "text-base"
: optionsFontsize === "lg"
? "text-lg"
: optionsFontsize === "xl"
? "text-xl"
: optionsFontsize === "2xl"
? "text-2xl"
: ""
} ${optionsClassName || ""}`}
>
<Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
<div className="py-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
className={({ active }) =>
`${
active ? "bg-indigo-50" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-custom-text-100`
}
value={option.value}
>
{assignee && userAvatar(option.value)}
{option.element ?? option.display}
</Combobox.Option>
))
) : (
<p className="text-xs text-custom-text-200 px-2">
No {title.toLowerCase()} found
</p>
)
) : (
<p className="text-xs text-custom-text-200 px-2">Loading...</p>
)}
</div>
</Combobox.Options>
</Transition>
</>
)}
</Combobox>
);
};
export default SearchListbox;

View File

@ -1,15 +0,0 @@
type Value = any;
export type Props = {
title: string;
multiple?: boolean;
options?: Array<{ display: string; element?: JSX.Element; value: Value }>;
onChange: (value: Value) => void;
value: Value;
icon?: JSX.Element;
buttonClassName?: string;
optionsClassName?: string;
width?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
optionsFontsize?: "sm" | "md" | "lg" | "xl" | "2xl";
assignee?: boolean;
};

View File

@ -1,55 +0,0 @@
import { useRouter } from "next/router";
// hooks
import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view";
import useUser from "hooks/use-user";
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
// types
import { IIssue } from "types";
type Props = { disableUserActions: boolean };
export const ViewIssuesGanttChartView: React.FC<Props> = ({ disableUserActions }) => {
const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query;
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { ganttIssues, mutateGanttIssues } = useGanttChartViewIssues(
workspaceSlug as string,
projectId as string,
viewId as string
);
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
handleMutation={() => mutateGanttIssues()}
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) => {}}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={isAllowed}
/>
</div>
</>
);
};

View File

@ -1,6 +1,5 @@
export * from "./delete-view-modal";
export * from "./form";
export * from "./gantt-chart";
export * from "./modal";
export * from "./select-filters";
export * from "./view-list-item";

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { observer } from "mobx-react-lite";
@ -26,12 +26,20 @@ export const GlobalViewsHeader: React.FC = observer(() => {
workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null
);
const isTabSelected = (tabKey: string) => router.pathname.includes(tabKey);
// bring the active view to the centre of the header
useEffect(() => {
if (!globalViewId) return;
const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`);
if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" });
}, [globalViewId]);
const isTabSelected = (tabKey: string) => router.pathname.includes(tabKey);
return (
<>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
<div className="group flex items-center px-4 overflow-x-scroll relative border-b border-custom-border-200">
<div className="group flex items-center px-4 w-full overflow-x-scroll relative border-b border-custom-border-200">
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => (
<Link key={tab.key} href={`/${workspaceSlug}/workspace-views/${tab.key}`}>
<a
@ -49,6 +57,7 @@ export const GlobalViewsHeader: React.FC = observer(() => {
{globalViewsStore.globalViewsList?.map((view) => (
<Link key={view.id} href={`/${workspaceSlug}/workspace-views/${view.id}`}>
<a
id={`global-view-${view.id}`}
className={`border-b-2 p-3 text-sm font-medium outline-none whitespace-nowrap flex-shrink-0 ${
view.id === globalViewId
? "border-custom-primary-100 text-custom-primary-100"
@ -62,7 +71,7 @@ export const GlobalViewsHeader: React.FC = observer(() => {
<button
type="button"
className="flex items-center justify-center flex-shrink-0 sticky right-0 w-12 py-3 border-transparent bg-custom-background-100 hover:border-custom-border-200 hover:text-custom-text-400"
className="flex items-center justify-center flex-shrink-0 sticky -right-4 w-12 py-3 border-transparent bg-custom-background-100 hover:border-custom-border-200 hover:text-custom-text-400"
onClick={() => setCreateViewModal(true)}
>
<PlusIcon className="h-4 w-4 text-custom-primary-200" />

View File

@ -77,6 +77,7 @@ export const ISSUE_ORDER_BY_OPTIONS: {
{ key: "-created_at", title: "Last Created" },
{ key: "-updated_at", title: "Last Updated" },
{ key: "start_date", title: "Start Date" },
{ key: "target_date", title: "Due Date" },
{ key: "priority", title: "Priority" },
];
@ -253,7 +254,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
display_filters: {
group_by: ["state", "priority", "labels", "assignees", "created_by"],
sub_group_by: ["state", "priority", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority", "target_date"],
type: [null, "active", "backlog"],
},
extra_options: {

View File

@ -14,23 +14,22 @@ export const AppLayout: FC<IAppLayout> = (props) => {
const { children, header } = props;
return (
<div className="h-screen w-full">
<>
{/* <CommandPalette /> */}
<UserAuthWrapper>
<WorkspaceAuthWrapper>
<div className="flex w-full h-full">
<div className="relative flex h-screen w-full overflow-hidden">
<AppSidebar />
<div className="relative flex flex-col h-screen w-full overflow-hidden">
<div className="w-full">{header}</div>
<main className={`relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100`}>
<div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">{children}</div>
</div>
</main>
</div>
<main className="relative flex flex-col h-full w-full overflow-hidden bg-custom-background-100">
{/* <div className="relative w-full">{header}</div> */}
{header}
<div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">{children}</div>
</div>
</main>
</div>
</WorkspaceAuthWrapper>
</UserAuthWrapper>
</div>
</>
);
};

View File

@ -20,8 +20,6 @@ import cycleServices from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// components
import { AnalyticsProjectModal } from "components/analytics";
// ui
import { Button } from "@plane/ui";
import { CustomMenu, EmptyState } from "components/ui";

View File

@ -11,7 +11,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// helper
import { truncateText } from "helpers/string.helper";
// components
import { AllViews } from "components/core";
import { ProjectLayoutRoot } from "components/issues";
import { ProjectIssuesHeader } from "components/headers";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -72,7 +72,7 @@ const ProjectIssues: NextPage = () => {
bg="secondary"
>
<div className="h-full w-full flex flex-col">
<AllViews />
<ProjectLayoutRoot />
</div>
</ProjectAuthorizationWrapper>
);

View File

@ -16,6 +16,8 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { ExistingIssuesListModal } from "components/core";
import { ModuleDetailsSidebar } from "components/modules";
import { ModuleLayoutRoot } from "components/issues";
import { ModuleIssuesHeader } from "components/headers";
// ui
import { CustomMenu, EmptyState } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -27,8 +29,6 @@ import { truncateText } from "helpers/string.helper";
import { ISearchIssueResponse } from "types";
// fetch-keys
import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
import { ModuleAllLayouts } from "components/issues";
import { ModuleIssuesHeader } from "components/headers";
const SingleModule: React.FC = () => {
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
@ -141,7 +141,7 @@ const SingleModule: React.FC = () => {
moduleSidebar ? "mr-[24rem]" : ""
} duration-300`}
>
<ModuleAllLayouts />
<ModuleLayoutRoot />
</div>
<ModuleDetailsSidebar
module={moduleDetails}

View File

@ -8,7 +8,7 @@ import viewsService from "services/views.service";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { ProjectViewAllLayouts } from "components/issues";
import { ProjectViewLayoutRoot } from "components/issues";
// ui
import { CustomMenu, EmptyState } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -88,7 +88,7 @@ const SingleView: React.FC = () => {
}}
/>
) : (
<ProjectViewAllLayouts />
<ProjectViewLayoutRoot />
)}
</ProjectAuthorizationWrapper>
);

View File

@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { AppLayout } from "layouts/app-layout";
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalViewsAllLayouts } from "components/issues";
import { GlobalViewLayoutRoot } from "components/issues";
import { GlobalIssuesHeader } from "components/headers";
// types
import { NextPage } from "next";
@ -30,7 +30,7 @@ const GlobalViewIssues: NextPage = () => {
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full w-full flex flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<GlobalViewsAllLayouts />
<GlobalViewLayoutRoot />
</div>
</div>
</AppLayout>

View File

@ -1,7 +1,7 @@
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { GlobalViewsAllLayouts } from "components/issues";
import { GlobalViewLayoutRoot } from "components/issues";
// layouts
import { AppLayout } from "layouts/app-layout";
// types
@ -12,7 +12,7 @@ const GlobalViewAllIssues: NextPage = () => (
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full w-full flex flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<GlobalViewsAllLayouts type="all-issues" />
<GlobalViewLayoutRoot type="all-issues" />
</div>
</div>
</AppLayout>

View File

@ -1,7 +1,7 @@
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { GlobalViewsAllLayouts } from "components/issues";
import { GlobalViewLayoutRoot } from "components/issues";
// layouts
import { AppLayout } from "layouts/app-layout";
// types
@ -12,7 +12,7 @@ const GlobalViewAssignedIssues: NextPage = () => (
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full w-full flex flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<GlobalViewsAllLayouts type="assigned" />
<GlobalViewLayoutRoot type="assigned" />
</div>
</div>
</AppLayout>

Some files were not shown because too many files have changed in this diff Show More