forked from github/plane
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:
parent
404e6a0cfc
commit
8aebf0bbd2
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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";
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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";
|
@ -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" />}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
@ -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";
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
export * from "./all-lists";
|
||||
export * from "./single-issue";
|
||||
export * from "./single-list";
|
||||
export * from "./inline-create-issue-form";
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,3 +1,2 @@
|
||||
export * from "./blocks";
|
||||
export * from "./cycle-issues-layout";
|
||||
export * from "./cycles-list-layout";
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
|
4
web/components/inbox/modals/index.ts
Normal file
4
web/components/inbox/modals/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./accept-issue-modal";
|
||||
export * from "./decline-issue-modal";
|
||||
export * from "./delete-issue-modal";
|
||||
export * from "./select-duplicate";
|
@ -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
|
@ -1,6 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export const IssueCalendarViewRoot = () => {
|
||||
console.log();
|
||||
return <div>IssueCalendarViewRoot</div>;
|
||||
};
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,6 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export const IssueGanttViewRoot = () => {
|
||||
console.log();
|
||||
return <div>IssueGanttViewRoot</div>;
|
||||
};
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
export * from "./root";
|
||||
export * from "./list";
|
||||
export * from "./item";
|
||||
export * from "./group-header";
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>;
|
||||
});
|
@ -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>
|
||||
);
|
||||
});
|
@ -1 +0,0 @@
|
||||
export * from "./priority-select";
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
||||
});
|
@ -1,6 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export const IssueSpreadsheetViewRoot = () => {
|
||||
console.log();
|
||||
return <div>IssueSpreadsheetViewRoot</div>;
|
||||
};
|
@ -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";
|
||||
|
@ -0,0 +1,4 @@
|
||||
export * from "./cycle-root";
|
||||
export * from "./module-root";
|
||||
export * from "./project-view-root";
|
||||
export * from "./project-root";
|
@ -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";
|
||||
|
@ -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";
|
@ -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;
|
||||
|
@ -87,6 +87,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
order_by: val,
|
||||
})
|
||||
}
|
||||
orderByOptions={layoutDisplayFiltersOptions?.display_filters.order_by ?? []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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}
|
||||
|
@ -23,7 +23,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<Popover as="div">
|
||||
{({ open }) => {
|
||||
if (open) {
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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" ? (
|
@ -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}
|
5
web/components/issues/issue-layouts/roots/index.ts
Normal file
5
web/components/issues/issue-layouts/roots/index.ts
Normal 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";
|
@ -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" ? (
|
@ -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" ? (
|
@ -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" ? (
|
@ -1,3 +1,2 @@
|
||||
export * from "./blocks";
|
||||
export * from "./module-issues-layout";
|
||||
export * from "./modules-list-layout";
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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;
|
15
web/components/search-listbox/types.d.ts
vendored
15
web/components/search-listbox/types.d.ts
vendored
@ -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;
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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";
|
||||
|
@ -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" />
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user