forked from github/plane
fix: mutation for issue update on both kanban & list (#436)
* refactor: issues filter logic * fix: removed fetch logic from hooks * feat: filter by assignee and label * chore: remove filter buttons * feat: filter options * fix: mutation for issue update on both kanban & list --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
636e8e6c60
commit
928ebdf632
@ -1,16 +1,14 @@
|
|||||||
// hooks
|
// hooks
|
||||||
import useIssueView from "hooks/use-issue-view";
|
import useProjectIssuesView from "hooks/use-issues-view";
|
||||||
// components
|
// components
|
||||||
import { SingleBoard } from "components/core/board-view/single-board";
|
import { SingleBoard } from "components/core/board-view/single-board";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IProjectMember, IState, UserAuth } from "types";
|
import { IIssue, IState, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: "issue" | "cycle" | "module";
|
type: "issue" | "cycle" | "module";
|
||||||
issues: IIssue[];
|
|
||||||
states: IState[] | undefined;
|
states: IState[] | undefined;
|
||||||
members: IProjectMember[] | undefined;
|
addIssueToState: (groupTitle: string) => void;
|
||||||
addIssueToState: (groupTitle: string, stateId: string | null) => void;
|
|
||||||
makeIssueCopy: (issue: IIssue) => void;
|
makeIssueCopy: (issue: IIssue) => void;
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
@ -22,9 +20,7 @@ type Props = {
|
|||||||
|
|
||||||
export const AllBoards: React.FC<Props> = ({
|
export const AllBoards: React.FC<Props> = ({
|
||||||
type,
|
type,
|
||||||
issues,
|
|
||||||
states,
|
states,
|
||||||
members,
|
|
||||||
addIssueToState,
|
addIssueToState,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
handleEditIssue,
|
handleEditIssue,
|
||||||
@ -34,56 +30,35 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
removeIssue,
|
removeIssue,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssueView(issues);
|
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useProjectIssuesView();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{groupedByIssues ? (
|
{groupedByIssues ? (
|
||||||
<div className="h-[calc(100vh-140px)] w-full">
|
<div className="horizontal-scroll-enable flex h-[calc(100vh-140px)] gap-x-4">
|
||||||
<div className="horizontal-scroll-enable flex h-full gap-x-3.5 overflow-x-auto overflow-y-hidden">
|
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
||||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
const currentState =
|
||||||
const currentState =
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const stateId =
|
return (
|
||||||
selectedGroup === "state_detail.name"
|
<SingleBoard
|
||||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
key={index}
|
||||||
: null;
|
type={type}
|
||||||
|
currentState={currentState}
|
||||||
const bgColor =
|
groupTitle={singleGroup}
|
||||||
selectedGroup === "state_detail.name"
|
handleEditIssue={handleEditIssue}
|
||||||
? states?.find((s) => s.name === singleGroup)?.color
|
makeIssueCopy={makeIssueCopy}
|
||||||
: "#000000";
|
addIssueToState={() => addIssueToState(singleGroup)}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
return (
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
<SingleBoard
|
handleTrashBox={handleTrashBox}
|
||||||
key={index}
|
removeIssue={removeIssue}
|
||||||
type={type}
|
userAuth={userAuth}
|
||||||
currentState={currentState}
|
/>
|
||||||
bgColor={bgColor}
|
);
|
||||||
groupTitle={singleGroup}
|
})}
|
||||||
groupedByIssues={groupedByIssues}
|
|
||||||
selectedGroup={selectedGroup}
|
|
||||||
members={members}
|
|
||||||
handleEditIssue={handleEditIssue}
|
|
||||||
makeIssueCopy={makeIssueCopy}
|
|
||||||
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
|
||||||
orderBy={orderBy}
|
|
||||||
handleTrashBox={handleTrashBox}
|
|
||||||
removeIssue={removeIssue}
|
|
||||||
userAuth={userAuth}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : null}
|
||||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,52 +1,42 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { getStateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IProjectMember, IState, NestedKeyOf } from "types";
|
import { IState } from "types";
|
||||||
import { getStateGroupIcon } from "components/icons";
|
|
||||||
type Props = {
|
type Props = {
|
||||||
groupedByIssues: {
|
|
||||||
[key: string]: IIssue[];
|
|
||||||
};
|
|
||||||
currentState?: IState | null;
|
currentState?: IState | null;
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
bgColor?: string;
|
|
||||||
addIssueToState: () => void;
|
addIssueToState: () => void;
|
||||||
members: IProjectMember[] | undefined;
|
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BoardHeader: React.FC<Props> = ({
|
export const BoardHeader: React.FC<Props> = ({
|
||||||
groupedByIssues,
|
|
||||||
currentState,
|
currentState,
|
||||||
selectedGroup,
|
|
||||||
groupTitle,
|
groupTitle,
|
||||||
bgColor,
|
|
||||||
addIssueToState,
|
addIssueToState,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
setIsCollapsed,
|
setIsCollapsed,
|
||||||
members,
|
|
||||||
}) => {
|
}) => {
|
||||||
const createdBy =
|
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
|
||||||
selectedGroup === "created_by"
|
|
||||||
? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..."
|
|
||||||
: null;
|
|
||||||
|
|
||||||
let assignees: any;
|
let bgColor = "#000000";
|
||||||
if (selectedGroup === "assignees") {
|
if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000";
|
||||||
assignees = groupTitle && groupTitle !== "" ? groupTitle.split(",") : [];
|
|
||||||
assignees =
|
if (selectedGroup === "priority")
|
||||||
assignees.length > 0
|
groupTitle === "high"
|
||||||
? assignees
|
? (bgColor = "#dc2626")
|
||||||
.map((a: string) => members?.find((m) => m.member.id === a)?.member.first_name)
|
: groupTitle === "medium"
|
||||||
.join(", ")
|
? (bgColor = "#f97316")
|
||||||
: "No assignee";
|
: groupTitle === "low"
|
||||||
}
|
? (bgColor = "#22c55e")
|
||||||
|
: (bgColor = "#ff0000");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -67,14 +57,12 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedGroup === "created_by"
|
{selectedGroup === "state"
|
||||||
? createdBy
|
? addSpaceIfCamelCase(currentState?.name ?? "")
|
||||||
: selectedGroup === "assignees"
|
|
||||||
? assignees
|
|
||||||
: addSpaceIfCamelCase(groupTitle)}
|
: addSpaceIfCamelCase(groupTitle)}
|
||||||
</h2>
|
</h2>
|
||||||
<span className="ml-0.5 rounded-full bg-gray-100 py-1 px-3 text-sm">
|
<span className="ml-0.5 rounded-full bg-gray-100 py-1 px-3 text-sm">
|
||||||
{groupedByIssues[groupTitle].length}
|
{groupedByIssues?.[groupTitle].length ?? 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@ import { useRouter } from "next/router";
|
|||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// hooks
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
// components
|
// components
|
||||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
import { BoardHeader, SingleBoardIssue } from "components/core";
|
||||||
@ -16,24 +17,17 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
|||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
|
import { IIssue, IState, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
type?: "issue" | "cycle" | "module";
|
||||||
currentState?: IState | null;
|
currentState?: IState | null;
|
||||||
bgColor?: string;
|
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
groupedByIssues: {
|
|
||||||
[key: string]: IIssue[];
|
|
||||||
};
|
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
|
||||||
members: IProjectMember[] | undefined;
|
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
makeIssueCopy: (issue: IIssue) => void;
|
makeIssueCopy: (issue: IIssue) => void;
|
||||||
addIssueToState: () => void;
|
addIssueToState: () => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
orderBy: NestedKeyOf<IIssue> | null;
|
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
removeIssue: ((bridgeId: string) => void) | null;
|
removeIssue: ((bridgeId: string) => void) | null;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
@ -42,17 +36,12 @@ type Props = {
|
|||||||
export const SingleBoard: React.FC<Props> = ({
|
export const SingleBoard: React.FC<Props> = ({
|
||||||
type,
|
type,
|
||||||
currentState,
|
currentState,
|
||||||
bgColor,
|
|
||||||
groupTitle,
|
groupTitle,
|
||||||
groupedByIssues,
|
|
||||||
selectedGroup,
|
|
||||||
members,
|
|
||||||
handleEditIssue,
|
handleEditIssue,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
addIssueToState,
|
addIssueToState,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
orderBy,
|
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
userAuth,
|
userAuth,
|
||||||
@ -60,35 +49,24 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
// collapse/expand
|
// collapse/expand
|
||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
|
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssuesView();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
if (selectedGroup === "priority")
|
|
||||||
groupTitle === "high"
|
|
||||||
? (bgColor = "#dc2626")
|
|
||||||
: groupTitle === "medium"
|
|
||||||
? (bgColor = "#f97316")
|
|
||||||
: groupTitle === "low"
|
|
||||||
? (bgColor = "#22c55e")
|
|
||||||
: (bgColor = "#ff0000");
|
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}>
|
<div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}>
|
||||||
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
|
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
addIssueToState={addIssueToState}
|
addIssueToState={addIssueToState}
|
||||||
currentState={currentState}
|
currentState={currentState}
|
||||||
bgColor={bgColor}
|
|
||||||
selectedGroup={selectedGroup}
|
|
||||||
groupTitle={groupTitle}
|
groupTitle={groupTitle}
|
||||||
groupedByIssues={groupedByIssues}
|
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
setIsCollapsed={setIsCollapsed}
|
setIsCollapsed={setIsCollapsed}
|
||||||
members={members}
|
|
||||||
/>
|
/>
|
||||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
@ -115,14 +93,12 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{groupedByIssues[groupTitle].map((issue, index: number) => (
|
{groupedByIssues?.[groupTitle].map((issue, index) => (
|
||||||
<Draggable
|
<Draggable
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
draggableId={issue.id}
|
draggableId={issue.id}
|
||||||
index={index}
|
index={index}
|
||||||
isDragDisabled={
|
isDragDisabled={isNotAllowed}
|
||||||
isNotAllowed || selectedGroup === "created_by" || selectedGroup === "assignees"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<SingleBoardIssue
|
<SingleBoardIssue
|
||||||
@ -130,16 +106,17 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
provided={provided}
|
provided={provided}
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
type={type}
|
type={type}
|
||||||
issue={issue}
|
index={index}
|
||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
|
issue={issue}
|
||||||
|
groupTitle={groupTitle}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
editIssue={() => handleEditIssue(issue)}
|
editIssue={() => handleEditIssue(issue)}
|
||||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
makeIssueCopy={() => makeIssueCopy(issue)}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
orderBy={orderBy}
|
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
removeIssue && removeIssue(issue.bridge);
|
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id);
|
||||||
}}
|
}}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
/>
|
/>
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
@ -33,31 +34,30 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import {
|
import { IIssue, Properties, UserAuth } from "types";
|
||||||
CycleIssueResponse,
|
|
||||||
IIssue,
|
|
||||||
ModuleIssueResponse,
|
|
||||||
NestedKeyOf,
|
|
||||||
Properties,
|
|
||||||
UserAuth,
|
|
||||||
} from "types";
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
import {
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: string;
|
type?: string;
|
||||||
provided: DraggableProvided;
|
provided: DraggableProvided;
|
||||||
snapshot: DraggableStateSnapshot;
|
snapshot: DraggableStateSnapshot;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
|
groupTitle?: string;
|
||||||
|
index: number;
|
||||||
|
selectedGroup: "priority" | "state" | "labels" | null;
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
orderBy: NestedKeyOf<IIssue> | null;
|
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
@ -67,13 +67,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
provided,
|
provided,
|
||||||
snapshot,
|
snapshot,
|
||||||
issue,
|
issue,
|
||||||
selectedGroup,
|
|
||||||
properties,
|
properties,
|
||||||
|
index,
|
||||||
|
selectedGroup,
|
||||||
editIssue,
|
editIssue,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
|
groupTitle,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
orderBy,
|
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
@ -81,6 +82,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
const [contextMenu, setContextMenu] = useState(false);
|
const [contextMenu, setContextMenu] = useState(false);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const { orderBy } = useIssuesView();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
@ -91,75 +94,55 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
if (cycleId)
|
if (cycleId)
|
||||||
mutate<CycleIssueResponse[]>(
|
mutate<
|
||||||
CYCLE_ISSUES(cycleId as string),
|
| {
|
||||||
(prevData) => {
|
[key: string]: IIssue[];
|
||||||
const updatedIssues = (prevData ?? []).map((p) => {
|
}
|
||||||
if (p.issue_detail.id === issue.id) {
|
| IIssue[]
|
||||||
return {
|
>(
|
||||||
...p,
|
CYCLE_ISSUES_WITH_PARAMS(cycleId as string),
|
||||||
issue_detail: {
|
(prevData) =>
|
||||||
...p.issue_detail,
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
...formData,
|
|
||||||
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
if (moduleId)
|
if (moduleId)
|
||||||
mutate<ModuleIssueResponse[]>(
|
mutate<
|
||||||
MODULE_ISSUES(moduleId as string),
|
| {
|
||||||
(prevData) => {
|
[key: string]: IIssue[];
|
||||||
const updatedIssues = (prevData ?? []).map((p) => {
|
}
|
||||||
if (p.issue_detail.id === issue.id) {
|
| IIssue[]
|
||||||
return {
|
>(
|
||||||
...p,
|
MODULE_ISSUES_WITH_PARAMS(moduleId as string),
|
||||||
issue_detail: {
|
(prevData) =>
|
||||||
...p.issue_detail,
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
...formData,
|
|
||||||
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
mutate<IIssue[]>(
|
mutate<
|
||||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
| IIssue[]
|
||||||
|
>(
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
(prevData ?? []).map((p) => {
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
if (p.id === issue.id)
|
|
||||||
return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list };
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}),
|
|
||||||
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string));
|
||||||
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string));
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[workspaceSlug, projectId, cycleId, moduleId, issue]
|
[workspaceSlug, projectId, cycleId, moduleId, issue, groupTitle, index, selectedGroup]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getStyle = (
|
const getStyle = (
|
||||||
@ -168,9 +151,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
) => {
|
) => {
|
||||||
if (orderBy === "sort_order") return style;
|
if (orderBy === "sort_order") return style;
|
||||||
if (!snapshot.isDragging) return {};
|
if (!snapshot.isDragging) return {};
|
||||||
if (!snapshot.isDropAnimating) {
|
if (!snapshot.isDropAnimating) return style;
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...style,
|
...style,
|
||||||
@ -301,7 +282,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
{properties.labels && issue.label_details.length > 0 && (
|
{properties.labels && issue.label_details.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{issue.label_details.map((label) => (
|
{issue.label_details.map((label) => (
|
||||||
<span
|
<div
|
||||||
key={label.id}
|
key={label.id}
|
||||||
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
|
||||||
>
|
>
|
||||||
@ -312,7 +293,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{label.name}
|
{label.name}
|
||||||
</span>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -10,7 +10,7 @@ import issuesService from "services/issues.service";
|
|||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
import useIssueView from "hooks/use-issue-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
@ -29,11 +29,7 @@ import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fet
|
|||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||||
import { PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
|
|
||||||
type Props = {
|
export const IssuesFilterView: React.FC = () => {
|
||||||
issues?: IIssue[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -44,12 +40,12 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
|
|||||||
groupByProperty,
|
groupByProperty,
|
||||||
setGroupByProperty,
|
setGroupByProperty,
|
||||||
setOrderBy,
|
setOrderBy,
|
||||||
setFilterIssue,
|
|
||||||
orderBy,
|
orderBy,
|
||||||
filterIssue,
|
filters,
|
||||||
|
setFilters,
|
||||||
resetFilterToDefault,
|
resetFilterToDefault,
|
||||||
setNewFilterDefaultView,
|
setNewFilterDefaultView,
|
||||||
} = useIssueView(issues ?? []);
|
} = useIssuesView();
|
||||||
|
|
||||||
const [properties, setProperties] = useIssuesProperties(
|
const [properties, setProperties] = useIssuesProperties(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
@ -79,208 +75,182 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex items-center gap-2">
|
||||||
{issues && issues.length > 0 && (
|
<div className="flex items-center gap-x-1">
|
||||||
<div className="flex items-center gap-2">
|
<button
|
||||||
<div className="flex items-center gap-x-1">
|
type="button"
|
||||||
<button
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||||
type="button"
|
issueView === "list" ? "bg-gray-200" : ""
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
}`}
|
||||||
issueView === "list" ? "bg-gray-200" : ""
|
onClick={() => setIssueViewToList()}
|
||||||
}`}
|
>
|
||||||
onClick={() => setIssueViewToList()}
|
<ListBulletIcon className="h-4 w-4" />
|
||||||
>
|
</button>
|
||||||
<ListBulletIcon className="h-4 w-4" />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||||
type="button"
|
issueView === "kanban" ? "bg-gray-200" : ""
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
}`}
|
||||||
issueView === "kanban" ? "bg-gray-200" : ""
|
onClick={() => setIssueViewToKanban()}
|
||||||
}`}
|
>
|
||||||
onClick={() => setIssueViewToKanban()}
|
<Squares2X2Icon className="h-4 w-4" />
|
||||||
>
|
</button>
|
||||||
<Squares2X2Icon className="h-4 w-4" />
|
</div>
|
||||||
</button>
|
<CustomMenu
|
||||||
</div>
|
customButton={
|
||||||
<CustomMenu
|
<button
|
||||||
label={
|
type="button"
|
||||||
<span className="flex items-center gap-2 rounded-md py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none">
|
className="group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none"
|
||||||
Filters
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<h4 className="px-1 py-2 font-medium">Status</h4>
|
Filters
|
||||||
{statesList?.map((state) => (
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
<CustomMenu.MenuItem onClick={() => {}}>
|
</button>
|
||||||
<>{state.name}</>
|
}
|
||||||
</CustomMenu.MenuItem>
|
optionsPosition="right"
|
||||||
))}
|
>
|
||||||
<h4 className="px-1 py-2 font-medium">Members</h4>
|
<CustomMenu.MenuItem
|
||||||
{members?.map((member) => (
|
onClick={() =>
|
||||||
<CustomMenu.MenuItem onClick={() => {}}>
|
setFilters({
|
||||||
<>
|
assignees: ["72d6ad43-41ff-4907-9980-2f5ee8745ad3"],
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
})
|
||||||
? member.member.first_name + " " + member.member.last_name
|
}
|
||||||
: member.member.email}
|
>
|
||||||
</>
|
Member- Aaryan
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
))}
|
</CustomMenu>
|
||||||
<h4 className="px-1 py-2 font-medium">Labels</h4>
|
<Popover className="relative">
|
||||||
{issueLabels?.map((label) => (
|
{({ open }) => (
|
||||||
<CustomMenu.MenuItem onClick={() => {}}>
|
<>
|
||||||
<>{label.name}</>
|
<Popover.Button
|
||||||
</CustomMenu.MenuItem>
|
className={`group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${
|
||||||
))}
|
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
|
||||||
<h4 className="px-1 py-2 font-medium">Priority</h4>
|
}`}
|
||||||
{PRIORITIES?.map((priority) => (
|
>
|
||||||
<CustomMenu.MenuItem onClick={() => {}}>
|
View
|
||||||
<span className="capitalize">{priority ?? "None"}</span>
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
</CustomMenu.MenuItem>
|
</Popover.Button>
|
||||||
))}
|
|
||||||
</CustomMenu>
|
|
||||||
<Popover className="relative">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Popover.Button
|
|
||||||
className={`group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${
|
|
||||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span>View</span>
|
|
||||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</Popover.Button>
|
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
enter="transition ease-out duration-200"
|
enter="transition ease-out duration-200"
|
||||||
enterFrom="opacity-0 translate-y-1"
|
enterFrom="opacity-0 translate-y-1"
|
||||||
enterTo="opacity-100 translate-y-0"
|
enterTo="opacity-100 translate-y-0"
|
||||||
leave="transition ease-in duration-150"
|
leave="transition ease-in duration-150"
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2">
|
<div className="relative divide-y-2">
|
||||||
{issues && (
|
<div className="space-y-4 pb-3">
|
||||||
<div className="space-y-4 pb-3">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<h4 className="text-sm text-gray-600">Group by</h4>
|
||||||
<h4 className="text-sm text-gray-600">Group by</h4>
|
<CustomMenu
|
||||||
<CustomMenu
|
label={
|
||||||
label={
|
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
|
||||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
"Select"
|
||||||
?.name ?? "Select"
|
}
|
||||||
}
|
width="lg"
|
||||||
width="lg"
|
>
|
||||||
|
{GROUP_BY_OPTIONS.map((option) =>
|
||||||
|
issueView === "kanban" && option.key === null ? null : (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() => setGroupByProperty(option.key)}
|
||||||
>
|
>
|
||||||
{GROUP_BY_OPTIONS.map((option) =>
|
{option.name}
|
||||||
issueView === "kanban" && option.key === null ? null : (
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
)
|
||||||
key={option.key}
|
)}
|
||||||
onClick={() => setGroupByProperty(option.key)}
|
</CustomMenu>
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-sm text-gray-600">Order by</h4>
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
|
||||||
"Select"
|
|
||||||
}
|
|
||||||
width="lg"
|
|
||||||
>
|
|
||||||
{ORDER_BY_OPTIONS.map((option) =>
|
|
||||||
groupByProperty === "priority" &&
|
|
||||||
option.key === "priority" ? null : (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={option.key}
|
|
||||||
onClick={() => {
|
|
||||||
setOrderBy(option.key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-sm text-gray-600">Issue type</h4>
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filterIssue)
|
|
||||||
?.name ?? "Select"
|
|
||||||
}
|
|
||||||
width="lg"
|
|
||||||
>
|
|
||||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={option.key}
|
|
||||||
onClick={() => setFilterIssue(option.key)}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
))}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-end gap-x-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-xs"
|
|
||||||
onClick={() => resetFilterToDefault()}
|
|
||||||
>
|
|
||||||
Reset to default
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-xs font-medium text-theme"
|
|
||||||
onClick={() => setNewFilterDefaultView()}
|
|
||||||
>
|
|
||||||
Set as default
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="space-y-2 py-3">
|
|
||||||
<h4 className="text-sm text-gray-600">Display Properties</h4>
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
{Object.keys(properties).map((key) => {
|
|
||||||
if (
|
|
||||||
issueView === "kanban" &&
|
|
||||||
((groupByProperty === "state_detail.name" && key === "state") ||
|
|
||||||
(groupByProperty === "priority" && key === "priority"))
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
type="button"
|
|
||||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
|
||||||
properties[key as keyof Properties]
|
|
||||||
? "border-theme bg-theme text-white"
|
|
||||||
: "border-gray-300"
|
|
||||||
}`}
|
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
|
||||||
>
|
|
||||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Popover.Panel>
|
<div className="flex items-center justify-between">
|
||||||
</Transition>
|
<h4 className="text-sm text-gray-600">Order by</h4>
|
||||||
</>
|
<CustomMenu
|
||||||
)}
|
label={
|
||||||
</Popover>
|
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||||
</div>
|
"Select"
|
||||||
)}
|
}
|
||||||
</>
|
width="lg"
|
||||||
|
>
|
||||||
|
{ORDER_BY_OPTIONS.map((option) =>
|
||||||
|
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() => {
|
||||||
|
setOrderBy(option.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-sm text-gray-600">Issue type</h4>
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||||
|
?.name ?? "Select"
|
||||||
|
}
|
||||||
|
width="lg"
|
||||||
|
>
|
||||||
|
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
type: option.key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-end gap-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => resetFilterToDefault()}
|
||||||
|
>
|
||||||
|
Reset to default
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs font-medium text-theme"
|
||||||
|
onClick={() => setNewFilterDefaultView()}
|
||||||
|
>
|
||||||
|
Set as default
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 py-3">
|
||||||
|
<h4 className="text-sm text-gray-600">Display Properties</h4>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{Object.keys(properties).map((key) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
|
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||||
|
properties[key as keyof Properties]
|
||||||
|
? "border-theme bg-theme text-white"
|
||||||
|
: "border-gray-300"
|
||||||
|
}`}
|
||||||
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
|
>
|
||||||
|
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,13 +12,13 @@ import stateService from "services/state.service";
|
|||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import modulesService from "services/modules.service";
|
import modulesService from "services/modules.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssueView from "hooks/use-issue-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// components
|
// components
|
||||||
import { AllLists, AllBoards } from "components/core";
|
import { AllLists, AllBoards } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
// icons
|
// icons
|
||||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon, RectangleStackIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { getStatesList } from "helpers/state.helper";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
// types
|
// types
|
||||||
@ -26,32 +26,29 @@ import { CycleIssueResponse, IIssue, ModuleIssueResponse, UserAuth } from "types
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
CYCLE_ISSUES,
|
CYCLE_ISSUES,
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
MODULE_ISSUES,
|
MODULE_ISSUES,
|
||||||
PROJECT_ISSUES_LIST,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
PROJECT_MEMBERS,
|
PROJECT_MEMBERS,
|
||||||
STATE_LIST,
|
STATE_LIST,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
import { EmptySpace, EmptySpaceItem } from "components/ui";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
type?: "issue" | "cycle" | "module";
|
||||||
issues: IIssue[];
|
|
||||||
openIssuesListModal?: () => void;
|
openIssuesListModal?: () => void;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesView: React.FC<Props> = ({
|
export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModal, userAuth }) => {
|
||||||
type = "issue",
|
|
||||||
issues,
|
|
||||||
openIssuesListModal,
|
|
||||||
userAuth,
|
|
||||||
}) => {
|
|
||||||
// create issue modal
|
// create issue modal
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
const [preloadedData, setPreloadedData] = useState<
|
||||||
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
// updates issue modal
|
// update issue modal
|
||||||
const [editIssueModal, setEditIssueModal] = useState(false);
|
const [editIssueModal, setEditIssueModal] = useState(false);
|
||||||
const [issueToEdit, setIssueToEdit] = useState<
|
const [issueToEdit, setIssueToEdit] = useState<
|
||||||
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
||||||
@ -68,11 +65,13 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issueView,
|
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
|
issueView,
|
||||||
groupByProperty: selectedGroup,
|
groupByProperty: selectedGroup,
|
||||||
orderBy,
|
orderBy,
|
||||||
} = useIssueView(issues);
|
filters,
|
||||||
|
setFilters,
|
||||||
|
} = useIssuesView();
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||||
@ -101,7 +100,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
(result: DropResult) => {
|
(result: DropResult) => {
|
||||||
setTrashBox(false);
|
setTrashBox(false);
|
||||||
|
|
||||||
if (!result.destination || !workspaceSlug || !projectId) return;
|
if (!result.destination || !workspaceSlug || !projectId || !groupedByIssues) return;
|
||||||
|
|
||||||
const { source, destination } = result;
|
const { source, destination } = result;
|
||||||
|
|
||||||
@ -156,90 +155,99 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
draggedItem.sort_order = newSortOrder;
|
draggedItem.sort_order = newSortOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orderBy === "sort_order" || source.droppableId !== destination.droppableId) {
|
const destinationGroup = destination.droppableId; // destination group id
|
||||||
const sourceGroup = source.droppableId; // source group id
|
|
||||||
const destinationGroup = destination.droppableId; // destination group id
|
|
||||||
|
|
||||||
if (!sourceGroup || !destinationGroup) return;
|
if (orderBy === "sort_order" || source.droppableId !== destination.droppableId) {
|
||||||
|
// different group/column;
|
||||||
|
|
||||||
|
// source.droppableId !== destination.droppableId -> even if order by is not sort_order,
|
||||||
|
// if the issue is moved to a different group, then we will change the group of the
|
||||||
|
// dragged item(or issue)
|
||||||
|
|
||||||
if (selectedGroup === "priority") draggedItem.priority = destinationGroup;
|
if (selectedGroup === "priority") draggedItem.priority = destinationGroup;
|
||||||
else if (selectedGroup === "state_detail.name") {
|
else if (selectedGroup === "state") draggedItem.state = destinationGroup;
|
||||||
const destinationState = states?.find((s) => s.name === destinationGroup);
|
}
|
||||||
|
|
||||||
if (!destinationState) return;
|
const sourceGroup = source.droppableId; // source group id
|
||||||
|
|
||||||
draggedItem.state = destinationState.id;
|
// TODO: move this mutation logic to a separate function
|
||||||
draggedItem.state_detail = destinationState;
|
if (cycleId)
|
||||||
}
|
mutate<{
|
||||||
|
[key: string]: IIssue[];
|
||||||
if (cycleId)
|
}>(
|
||||||
mutate<CycleIssueResponse[]>(
|
CYCLE_ISSUES_WITH_PARAMS(cycleId as string),
|
||||||
CYCLE_ISSUES(cycleId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
const updatedIssues = prevData.map((issue) => {
|
|
||||||
if (issue.issue_detail.id === draggedItem.id) {
|
|
||||||
return {
|
|
||||||
...issue,
|
|
||||||
issue_detail: draggedItem,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (moduleId)
|
|
||||||
mutate<ModuleIssueResponse[]>(
|
|
||||||
MODULE_ISSUES(moduleId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
const updatedIssues = prevData.map((issue) => {
|
|
||||||
if (issue.issue_detail.id === draggedItem.id) {
|
|
||||||
return {
|
|
||||||
...issue,
|
|
||||||
issue_detail: draggedItem,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate<IIssue[]>(
|
|
||||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
const updatedIssues = prevData.map((i) => {
|
const sourceGroupArray = prevData[sourceGroup];
|
||||||
if (i.id === draggedItem.id) return draggedItem;
|
const destinationGroupArray = prevData[destinationGroup];
|
||||||
|
|
||||||
return i;
|
sourceGroupArray.splice(source.index, 1);
|
||||||
});
|
destinationGroupArray.splice(destination.index, 0, draggedItem);
|
||||||
|
|
||||||
return updatedIssues;
|
return {
|
||||||
|
...prevData,
|
||||||
|
[sourceGroup]: sourceGroupArray,
|
||||||
|
[destinationGroup]: destinationGroupArray,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
else if (moduleId)
|
||||||
|
mutate<{
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}>(
|
||||||
|
MODULE_ISSUES_WITH_PARAMS(moduleId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
const sourceGroupArray = prevData[sourceGroup];
|
||||||
|
const destinationGroupArray = prevData[destinationGroup];
|
||||||
|
|
||||||
|
sourceGroupArray.splice(source.index, 1);
|
||||||
|
destinationGroupArray.splice(destination.index, 0, draggedItem);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
[sourceGroup]: sourceGroupArray,
|
||||||
|
[destinationGroup]: destinationGroupArray,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
else
|
||||||
|
mutate<{ [key: string]: IIssue[] }>(
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
const sourceGroupArray = prevData[sourceGroup];
|
||||||
|
const destinationGroupArray = prevData[destinationGroup];
|
||||||
|
|
||||||
|
sourceGroupArray.splice(source.index, 1);
|
||||||
|
destinationGroupArray.splice(destination.index, 0, draggedItem);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
[sourceGroup]: sourceGroupArray,
|
||||||
|
[destinationGroup]: destinationGroupArray,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
// patch request
|
// patch request
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||||
priority: draggedItem.priority,
|
priority: draggedItem.priority,
|
||||||
state: draggedItem.state,
|
state: draggedItem.state,
|
||||||
sort_order: draggedItem.sort_order,
|
sort_order: draggedItem.sort_order,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
||||||
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@ -250,17 +258,15 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
projectId,
|
projectId,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
orderBy,
|
orderBy,
|
||||||
states,
|
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addIssueToState = useCallback(
|
const addIssueToState = useCallback(
|
||||||
(groupTitle: string, stateId: string | null) => {
|
(groupTitle: string) => {
|
||||||
setCreateIssueModal(true);
|
setCreateIssueModal(true);
|
||||||
if (selectedGroup)
|
if (selectedGroup)
|
||||||
setPreloadedData({
|
setPreloadedData({
|
||||||
state: stateId ?? undefined,
|
|
||||||
[selectedGroup]: groupTitle,
|
[selectedGroup]: groupTitle,
|
||||||
actionType: "createIssue",
|
actionType: "createIssue",
|
||||||
});
|
});
|
||||||
@ -372,69 +378,116 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
isOpen={deleteIssueModal}
|
isOpen={deleteIssueModal}
|
||||||
data={issueToDelete}
|
data={issueToDelete}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative">
|
{Object.keys(filters).map((key) => {
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
if (filters[key as keyof typeof filters] !== null)
|
||||||
<StrictModeDroppable droppableId="trashBox">
|
return (
|
||||||
{(provided, snapshot) => (
|
<button
|
||||||
<div
|
key={key}
|
||||||
className={`${
|
type="button"
|
||||||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
className="rounded bg-black p-2 text-xs text-white"
|
||||||
} fixed top-9 right-9 z-20 flex h-28 w-96 items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${
|
onClick={() =>
|
||||||
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
|
setFilters({
|
||||||
} duration-200`}
|
[key]: null,
|
||||||
ref={provided.innerRef}
|
})
|
||||||
{...provided.droppableProps}
|
}
|
||||||
>
|
>
|
||||||
<TrashIcon className="h-4 w-4" />
|
Remove {key} filter
|
||||||
Drop issue here to delete
|
</button>
|
||||||
</div>
|
);
|
||||||
)}
|
})}
|
||||||
</StrictModeDroppable>
|
|
||||||
{issueView === "list" ? (
|
|
||||||
<AllLists
|
|
||||||
type={type}
|
|
||||||
issues={issues}
|
|
||||||
states={states}
|
|
||||||
members={members}
|
|
||||||
addIssueToState={addIssueToState}
|
|
||||||
makeIssueCopy={makeIssueCopy}
|
|
||||||
handleEditIssue={handleEditIssue}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
|
||||||
removeIssue={
|
|
||||||
type === "cycle"
|
|
||||||
? removeIssueFromCycle
|
|
||||||
: type === "module"
|
|
||||||
? removeIssueFromModule
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
userAuth={userAuth}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AllBoards
|
|
||||||
type={type}
|
|
||||||
issues={issues}
|
|
||||||
states={states}
|
|
||||||
members={members}
|
|
||||||
addIssueToState={addIssueToState}
|
|
||||||
makeIssueCopy={makeIssueCopy}
|
|
||||||
handleEditIssue={handleEditIssue}
|
|
||||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
handleTrashBox={handleTrashBox}
|
|
||||||
removeIssue={
|
|
||||||
type === "cycle"
|
|
||||||
? removeIssueFromCycle
|
|
||||||
: type === "module"
|
|
||||||
? removeIssueFromModule
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
userAuth={userAuth}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DragDropContext>
|
|
||||||
</div>
|
</div>
|
||||||
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
|
<StrictModeDroppable droppableId="trashBox">
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||||
|
} fixed top-9 right-9 z-20 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${
|
||||||
|
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
|
||||||
|
} duration-200`}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
Drop issue here to delete
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StrictModeDroppable>
|
||||||
|
{groupedByIssues ? (
|
||||||
|
Object.keys(groupedByIssues).length > 0 ? (
|
||||||
|
<>
|
||||||
|
{issueView === "list" ? (
|
||||||
|
<AllLists
|
||||||
|
type={type}
|
||||||
|
states={states}
|
||||||
|
members={members}
|
||||||
|
addIssueToState={addIssueToState}
|
||||||
|
makeIssueCopy={makeIssueCopy}
|
||||||
|
handleEditIssue={handleEditIssue}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
||||||
|
removeIssue={
|
||||||
|
type === "cycle"
|
||||||
|
? removeIssueFromCycle
|
||||||
|
: type === "module"
|
||||||
|
? removeIssueFromModule
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
userAuth={userAuth}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AllBoards
|
||||||
|
type={type}
|
||||||
|
states={states}
|
||||||
|
addIssueToState={addIssueToState}
|
||||||
|
makeIssueCopy={makeIssueCopy}
|
||||||
|
handleEditIssue={handleEditIssue}
|
||||||
|
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
handleTrashBox={handleTrashBox}
|
||||||
|
removeIssue={
|
||||||
|
type === "cycle"
|
||||||
|
? removeIssueFromCycle
|
||||||
|
: type === "module"
|
||||||
|
? removeIssueFromModule
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
userAuth={userAuth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
||||||
|
<EmptySpace
|
||||||
|
title="You don't have any issue yet."
|
||||||
|
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
||||||
|
Icon={RectangleStackIcon}
|
||||||
|
>
|
||||||
|
<EmptySpaceItem
|
||||||
|
title="Create a new issue"
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
Use <pre className="inline rounded bg-gray-200 px-2 py-1">C</pre> shortcut to
|
||||||
|
create a new issue
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
Icon={PlusIcon}
|
||||||
|
action={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", {
|
||||||
|
key: "c",
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EmptySpace>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<p className="text-center">Loading...</p>
|
||||||
|
)}
|
||||||
|
</DragDropContext>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// hooks
|
// hooks
|
||||||
import useIssueView from "hooks/use-issue-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// components
|
// components
|
||||||
import { SingleList } from "components/core/list-view/single-list";
|
import { SingleList } from "components/core/list-view/single-list";
|
||||||
// types
|
// types
|
||||||
@ -8,7 +8,6 @@ import { IIssue, IProjectMember, IState, UserAuth } from "types";
|
|||||||
// types
|
// types
|
||||||
type Props = {
|
type Props = {
|
||||||
type: "issue" | "cycle" | "module";
|
type: "issue" | "cycle" | "module";
|
||||||
issues: IIssue[];
|
|
||||||
states: IState[] | undefined;
|
states: IState[] | undefined;
|
||||||
members: IProjectMember[] | undefined;
|
members: IProjectMember[] | undefined;
|
||||||
addIssueToState: (groupTitle: string, stateId: string | null) => void;
|
addIssueToState: (groupTitle: string, stateId: string | null) => void;
|
||||||
@ -22,7 +21,6 @@ type Props = {
|
|||||||
|
|
||||||
export const AllLists: React.FC<Props> = ({
|
export const AllLists: React.FC<Props> = ({
|
||||||
type,
|
type,
|
||||||
issues,
|
|
||||||
states,
|
states,
|
||||||
members,
|
members,
|
||||||
addIssueToState,
|
addIssueToState,
|
||||||
@ -33,44 +31,35 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
removeIssue,
|
removeIssue,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const { groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-5">
|
<>
|
||||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
{groupedByIssues && (
|
||||||
const currentState =
|
<div className="flex flex-col space-y-5">
|
||||||
selectedGroup === "state_detail.name"
|
{Object.keys(groupedByIssues).map((singleGroup) => {
|
||||||
? states?.find((s) => s.name === singleGroup)
|
const stateId = selectedGroup === "state" ? singleGroup : null;
|
||||||
: null;
|
|
||||||
const stateId =
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
|
||||||
: null;
|
|
||||||
const bgColor =
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.color
|
|
||||||
: "#000000";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleList
|
<SingleList
|
||||||
key={singleGroup}
|
key={singleGroup}
|
||||||
type={type}
|
type={type}
|
||||||
currentState={currentState}
|
groupTitle={singleGroup}
|
||||||
bgColor={bgColor}
|
groupedByIssues={groupedByIssues}
|
||||||
groupTitle={singleGroup}
|
selectedGroup={selectedGroup}
|
||||||
groupedByIssues={groupedByIssues}
|
members={members}
|
||||||
selectedGroup={selectedGroup}
|
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
||||||
members={members}
|
makeIssueCopy={makeIssueCopy}
|
||||||
addIssueToState={() => addIssueToState(singleGroup, stateId)}
|
handleEditIssue={handleEditIssue}
|
||||||
makeIssueCopy={makeIssueCopy}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
handleEditIssue={handleEditIssue}
|
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
removeIssue={removeIssue}
|
||||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
userAuth={userAuth}
|
||||||
removeIssue={removeIssue}
|
/>
|
||||||
userAuth={userAuth}
|
);
|
||||||
/>
|
})}
|
||||||
);
|
</div>
|
||||||
})}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,8 @@ import {
|
|||||||
ViewPrioritySelect,
|
ViewPrioritySelect,
|
||||||
ViewStateSelect,
|
ViewStateSelect,
|
||||||
} from "components/issues/view-select";
|
} from "components/issues/view-select";
|
||||||
|
// hooks
|
||||||
|
import useIssueView from "hooks/use-issues-view";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -28,16 +29,23 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||||
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
// types
|
// types
|
||||||
import { CycleIssueResponse, IIssue, ModuleIssueResponse, Properties, UserAuth } from "types";
|
import { IIssue, Properties, UserAuth } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
import {
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: string;
|
type?: string;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
|
groupTitle?: string;
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
|
index: number;
|
||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
@ -49,8 +57,10 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
issue,
|
issue,
|
||||||
properties,
|
properties,
|
||||||
editIssue,
|
editIssue,
|
||||||
|
index,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
|
groupTitle,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
@ -63,80 +73,62 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { groupByProperty: selectedGroup } = useIssueView();
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
if (cycleId)
|
if (cycleId)
|
||||||
mutate<CycleIssueResponse[]>(
|
mutate<
|
||||||
CYCLE_ISSUES(cycleId as string),
|
| {
|
||||||
(prevData) => {
|
[key: string]: IIssue[];
|
||||||
const updatedIssues = (prevData ?? []).map((p) => {
|
}
|
||||||
if (p.issue_detail.id === issue.id) {
|
| IIssue[]
|
||||||
return {
|
>(
|
||||||
...p,
|
CYCLE_ISSUES_WITH_PARAMS(cycleId as string),
|
||||||
issue_detail: {
|
(prevData) =>
|
||||||
...p.issue_detail,
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
...formData,
|
|
||||||
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
if (moduleId)
|
if (moduleId)
|
||||||
mutate<ModuleIssueResponse[]>(
|
mutate<
|
||||||
MODULE_ISSUES(moduleId as string),
|
| {
|
||||||
(prevData) => {
|
[key: string]: IIssue[];
|
||||||
const updatedIssues = (prevData ?? []).map((p) => {
|
}
|
||||||
if (p.issue_detail.id === issue.id) {
|
| IIssue[]
|
||||||
return {
|
>(
|
||||||
...p,
|
MODULE_ISSUES_WITH_PARAMS(moduleId as string),
|
||||||
issue_detail: {
|
(prevData) =>
|
||||||
...p.issue_detail,
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
...formData,
|
|
||||||
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
mutate<IIssue[]>(
|
mutate<
|
||||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
| IIssue[]
|
||||||
|
>(
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string),
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
(prevData ?? []).map((p) => {
|
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
|
||||||
if (p.id === issue.id)
|
|
||||||
return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list };
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}),
|
|
||||||
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
|
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string));
|
||||||
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
|
if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string));
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[workspaceSlug, projectId, cycleId, moduleId, issue]
|
[workspaceSlug, projectId, cycleId, moduleId, issue, groupTitle, index, selectedGroup]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
|
@ -12,7 +12,7 @@ import { getStateGroupIcon } from "components/icons";
|
|||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
|
import { IIssue, IProjectMember, IState, UserAuth } from "types";
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -23,7 +23,7 @@ type Props = {
|
|||||||
groupedByIssues: {
|
groupedByIssues: {
|
||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
};
|
};
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
selectedGroup: "priority" | "state" | "labels" | null;
|
||||||
members: IProjectMember[] | undefined;
|
members: IProjectMember[] | undefined;
|
||||||
addIssueToState: () => void;
|
addIssueToState: () => void;
|
||||||
makeIssueCopy: (issue: IIssue) => void;
|
makeIssueCopy: (issue: IIssue) => void;
|
||||||
@ -55,22 +55,6 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
const createdBy =
|
|
||||||
selectedGroup === "created_by"
|
|
||||||
? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "Loading..."
|
|
||||||
: null;
|
|
||||||
|
|
||||||
let assignees: any;
|
|
||||||
if (selectedGroup === "assignees") {
|
|
||||||
assignees = groupTitle && groupTitle !== "" ? groupTitle.split(",") : [];
|
|
||||||
assignees =
|
|
||||||
assignees.length > 0
|
|
||||||
? assignees
|
|
||||||
.map((a: string) => members?.find((m) => m.member.id === a)?.member.first_name)
|
|
||||||
.join(", ")
|
|
||||||
: "No assignee";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure key={groupTitle} as="div" defaultOpen>
|
<Disclosure key={groupTitle} as="div" defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
@ -82,7 +66,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<Disclosure.Button>
|
<Disclosure.Button>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
{selectedGroup !== null && selectedGroup === "state_detail.name" ? (
|
{selectedGroup !== null && selectedGroup === "state" ? (
|
||||||
<span>
|
<span>
|
||||||
{currentState && getStateGroupIcon(currentState.group, "20", "20", bgColor)}
|
{currentState && getStateGroupIcon(currentState.group, "20", "20", bgColor)}
|
||||||
</span>
|
</span>
|
||||||
@ -91,11 +75,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{selectedGroup !== null ? (
|
{selectedGroup !== null ? (
|
||||||
<h2 className="text-xl font-semibold capitalize leading-6 text-gray-800">
|
<h2 className="text-xl font-semibold capitalize leading-6 text-gray-800">
|
||||||
{selectedGroup === "created_by"
|
{addSpaceIfCamelCase(groupTitle)}
|
||||||
? createdBy
|
|
||||||
: selectedGroup === "assignees"
|
|
||||||
? assignees
|
|
||||||
: addSpaceIfCamelCase(groupTitle)}
|
|
||||||
</h2>
|
</h2>
|
||||||
) : (
|
) : (
|
||||||
<h2 className="font-medium leading-5">All Issues</h2>
|
<h2 className="font-medium leading-5">All Issues</h2>
|
||||||
@ -105,7 +85,6 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
|
|
||||||
{type === "issue" ? (
|
{type === "issue" ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -145,17 +124,19 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
{groupedByIssues[groupTitle] ? (
|
{groupedByIssues[groupTitle] ? (
|
||||||
groupedByIssues[groupTitle].length > 0 ? (
|
groupedByIssues[groupTitle].length > 0 ? (
|
||||||
groupedByIssues[groupTitle].map((issue: IIssue) => (
|
groupedByIssues[groupTitle].map((issue, index) => (
|
||||||
<SingleListIssue
|
<SingleListIssue
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
type={type}
|
type={type}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
|
groupTitle={groupTitle}
|
||||||
|
index={index}
|
||||||
editIssue={() => handleEditIssue(issue)}
|
editIssue={() => handleEditIssue(issue)}
|
||||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
makeIssueCopy={() => makeIssueCopy(issue)}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
removeIssue && removeIssue(issue.bridge);
|
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id);
|
||||||
}}
|
}}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
/>
|
/>
|
||||||
|
@ -179,6 +179,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
{members?.map((member, index) => {
|
{members?.map((member, index) => {
|
||||||
const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id));
|
const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id));
|
||||||
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
||||||
|
|
||||||
if (totalArray.length > 0) {
|
if (totalArray.length > 0) {
|
||||||
return (
|
return (
|
||||||
<SingleProgressStats
|
<SingleProgressStats
|
||||||
@ -223,9 +224,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel as="div" className="flex w-full flex-col ">
|
<Tab.Panel as="div" className="flex w-full flex-col ">
|
||||||
{issueLabels?.map((issue, index) => {
|
{issueLabels?.map((label, index) => {
|
||||||
const totalArray = issues?.filter((i) => i.labels?.includes(issue.id));
|
const totalArray = issues?.filter((i) => i.labels?.includes(label.id));
|
||||||
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
||||||
|
|
||||||
if (totalArray.length > 0) {
|
if (totalArray.length > 0) {
|
||||||
return (
|
return (
|
||||||
<SingleProgressStats
|
<SingleProgressStats
|
||||||
@ -235,10 +237,11 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
<span
|
<span
|
||||||
className="block h-3 w-3 rounded-full "
|
className="block h-3 w-3 rounded-full "
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: issue.color,
|
backgroundColor:
|
||||||
|
label.color && label.color !== "" ? label.color : "#000000",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs capitalize">{issue.name}</span>
|
<span className="text-xs capitalize">{label.name}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
completed={completeArray.length}
|
completed={completeArray.length}
|
||||||
|
@ -6,14 +6,23 @@ type TSingleProgressStatsProps = {
|
|||||||
title: any;
|
title: any;
|
||||||
completed: number;
|
completed: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
onClick?: () => void;
|
||||||
|
selected?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
||||||
title,
|
title,
|
||||||
completed,
|
completed,
|
||||||
total,
|
total,
|
||||||
|
onClick,
|
||||||
|
selected = false,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="flex w-full items-center justify-between py-3 text-xs">
|
<div
|
||||||
|
className={`flex w-full items-center justify-between py-3 text-xs ${
|
||||||
|
onClick ? "cursor-pointer hover:bg-gray-100" : ""
|
||||||
|
} ${selected ? "bg-gray-100" : ""}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<div className="flex w-1/2 items-center justify-start gap-2">{title}</div>
|
<div className="flex w-1/2 items-center justify-start gap-2">{title}</div>
|
||||||
<div className="flex w-1/2 items-center justify-end gap-1 px-2">
|
<div className="flex w-1/2 items-center justify-end gap-1 px-2">
|
||||||
<div className="flex h-5 items-center justify-center gap-1 ">
|
<div className="flex h-5 items-center justify-center gap-1 ">
|
||||||
|
@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@ -36,25 +36,17 @@ import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helpe
|
|||||||
import { groupBy } from "helpers/array.helper";
|
import { groupBy } from "helpers/array.helper";
|
||||||
import { renderDateFormat, renderShortDate } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { CycleIssueResponse, ICycle, IIssue } from "types";
|
import { ICycle, IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
import { CYCLE_DETAILS, CYCLE_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues: IIssue[];
|
|
||||||
cycle: ICycle | undefined;
|
cycle: ICycle | undefined;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
cycleIssues: CycleIssueResponse[];
|
|
||||||
cycleStatus: string;
|
cycleStatus: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CycleDetailsSidebar: React.FC<Props> = ({
|
export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatus }) => {
|
||||||
issues,
|
|
||||||
cycle,
|
|
||||||
isOpen,
|
|
||||||
cycleIssues,
|
|
||||||
cycleStatus,
|
|
||||||
}) => {
|
|
||||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||||
const [startDateRange, setStartDateRange] = useState<Date | null>(new Date());
|
const [startDateRange, setStartDateRange] = useState<Date | null>(new Date());
|
||||||
const [endDateRange, setEndDateRange] = useState<Date | null>(null);
|
const [endDateRange, setEndDateRange] = useState<Date | null>(null);
|
||||||
@ -69,13 +61,25 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
end_date: new Date().toString(),
|
end_date: new Date().toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { data: issues } = useSWR<IIssue[]>(
|
||||||
|
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
|
||||||
|
workspaceSlug && projectId && cycleId
|
||||||
|
? () =>
|
||||||
|
cyclesService.getCycleIssues(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
cycleId as string
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const groupedIssues = {
|
const groupedIssues = {
|
||||||
backlog: [],
|
backlog: [],
|
||||||
unstarted: [],
|
unstarted: [],
|
||||||
started: [],
|
started: [],
|
||||||
cancelled: [],
|
cancelled: [],
|
||||||
completed: [],
|
completed: [],
|
||||||
...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"),
|
...groupBy(issues ?? [], "state_detail.group"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { reset } = useForm({
|
const { reset } = useForm({
|
||||||
@ -131,9 +135,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
const isStartValid = new Date(`${cycle?.start_date}`) <= new Date();
|
const isStartValid = new Date(`${cycle?.start_date}`) <= new Date();
|
||||||
const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`);
|
const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`);
|
||||||
|
|
||||||
const progressPercentage = cycleIssues
|
const progressPercentage = issues
|
||||||
? Math.round((groupedIssues.completed.length / cycleIssues?.length) * 100)
|
? Math.round((groupedIssues.completed.length / issues?.length) * 100)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteCycleModal isOpen={cycleDeleteModal} setIsOpen={setCycleDeleteModal} data={cycle} />
|
<DeleteCycleModal isOpen={cycleDeleteModal} setIsOpen={setCycleDeleteModal} data={cycle} />
|
||||||
@ -305,10 +310,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
<span className="h-4 w-4">
|
<span className="h-4 w-4">
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
value={groupedIssues.completed.length}
|
value={groupedIssues.completed.length}
|
||||||
maxValue={cycleIssues?.length}
|
maxValue={issues?.length}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{groupedIssues.completed.length}/{cycleIssues?.length}
|
{groupedIssues.completed.length}/{issues?.length}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -324,7 +329,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
<div className="flex w-full items-center justify-between gap-2 ">
|
<div className="flex w-full items-center justify-between gap-2 ">
|
||||||
<div className="flex items-center justify-start gap-2 text-sm">
|
<div className="flex items-center justify-start gap-2 text-sm">
|
||||||
<span className="font-medium text-gray-500">Progress</span>
|
<span className="font-medium text-gray-500">Progress</span>
|
||||||
{!open && cycleIssues && progressPercentage ? (
|
{!open && issues && progressPercentage ? (
|
||||||
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
|
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
|
||||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||||
</span>
|
</span>
|
||||||
@ -359,7 +364,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Pending Issues -{" "}
|
Pending Issues -{" "}
|
||||||
{cycleIssues?.length - groupedIssues.completed.length}{" "}
|
{issues?.length ?? 0 - groupedIssues.completed.length}{" "}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -376,7 +381,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative h-40 w-80">
|
<div className="relative h-40 w-80">
|
||||||
<ProgressChart
|
<ProgressChart
|
||||||
issues={issues}
|
issues={issues ?? []}
|
||||||
start={cycle?.start_date ?? ""}
|
start={cycle?.start_date ?? ""}
|
||||||
end={cycle?.end_date ?? ""}
|
end={cycle?.end_date ?? ""}
|
||||||
/>
|
/>
|
||||||
@ -403,7 +408,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
<span className="font-medium text-gray-500">Other Information</span>
|
<span className="font-medium text-gray-500">Other Information</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{issues.length > 0 ? (
|
{(issues?.length ?? 0) > 0 ? (
|
||||||
<Disclosure.Button>
|
<Disclosure.Button>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
|
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
|
||||||
@ -419,9 +424,12 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
{issues.length > 0 ? (
|
{(issues?.length ?? 0) > 0 ? (
|
||||||
<div className=" h-full w-full py-4">
|
<div className=" h-full w-full py-4">
|
||||||
<SidebarProgressStats issues={issues} groupedIssues={groupedIssues} />
|
<SidebarProgressStats
|
||||||
|
issues={issues ?? []}
|
||||||
|
groupedIssues={groupedIssues}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
|
@ -30,7 +30,6 @@ import { capitalizeFirstLetter, copyTextToClipboard, truncateText } from "helper
|
|||||||
import {
|
import {
|
||||||
CompletedCyclesResponse,
|
CompletedCyclesResponse,
|
||||||
CurrentAndUpcomingCyclesResponse,
|
CurrentAndUpcomingCyclesResponse,
|
||||||
CycleIssueResponse,
|
|
||||||
DraftCyclesResponse,
|
DraftCyclesResponse,
|
||||||
ICycle,
|
ICycle,
|
||||||
} from "types";
|
} from "types";
|
||||||
@ -65,7 +64,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
|
const { data: cycleIssues } = useSWR(
|
||||||
workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null,
|
workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null,
|
||||||
workspaceSlug && projectId && cycle.id
|
workspaceSlug && projectId && cycle.id
|
||||||
? () => cyclesService.getCycleIssues(workspaceSlug as string, projectId as string, cycle.id)
|
? () => cyclesService.getCycleIssues(workspaceSlug as string, projectId as string, cycle.id)
|
||||||
|
@ -28,6 +28,8 @@ export const ViewDueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue,
|
|||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
partialUpdateIssue({
|
partialUpdateIssue({
|
||||||
target_date: val,
|
target_date: val,
|
||||||
|
priority: issue.priority,
|
||||||
|
state: issue.state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
|
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
|
||||||
|
@ -25,8 +25,10 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
}) => (
|
}) => (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={issue.state}
|
value={issue.priority}
|
||||||
onChange={(data: string) => partialUpdateIssue({ priority: data })}
|
onChange={(data: string) =>
|
||||||
|
partialUpdateIssue({ priority: data, state: issue.state, target_date: issue.target_date })
|
||||||
|
}
|
||||||
maxHeight="md"
|
maxHeight="md"
|
||||||
customButton={
|
customButton={
|
||||||
<button
|
<button
|
||||||
|
@ -58,7 +58,13 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={issue.state}
|
value={issue.state}
|
||||||
onChange={(data: string) => partialUpdateIssue({ state: data })}
|
onChange={(data: string) =>
|
||||||
|
partialUpdateIssue({
|
||||||
|
state: data,
|
||||||
|
priority: issue.priority,
|
||||||
|
target_date: issue.target_date,
|
||||||
|
})
|
||||||
|
}
|
||||||
options={options}
|
options={options}
|
||||||
label={
|
label={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -33,11 +33,11 @@ import ProgressChart from "components/core/sidebar/progress-chart";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui";
|
import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderDateFormat, renderShortDate, timeAgo } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDate } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
|
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
|
||||||
import { groupBy } from "helpers/array.helper";
|
import { groupBy } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IModule, ModuleIssueResponse, ModuleLink, UserAuth } from "types";
|
import { IIssue, IModule, ModuleLink, UserAuth } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { MODULE_DETAILS } from "constants/fetch-keys";
|
import { MODULE_DETAILS } from "constants/fetch-keys";
|
||||||
// constant
|
// constant
|
||||||
@ -55,7 +55,7 @@ type Props = {
|
|||||||
issues: IIssue[];
|
issues: IIssue[];
|
||||||
module?: IModule;
|
module?: IModule;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
moduleIssues: ModuleIssueResponse[] | undefined;
|
moduleIssues?: IIssue[];
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ import { groupBy, orderArrayBy } from "helpers/array.helper";
|
|||||||
import { orderStateGroups } from "helpers/state.helper";
|
import { orderStateGroups } from "helpers/state.helper";
|
||||||
// types
|
// types
|
||||||
import { IState } from "types";
|
import { IState } from "types";
|
||||||
import { StateGroup } from "components/states";
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
import { getStateGroupIcon } from "components/icons";
|
import { getStateGroupIcon } from "components/icons";
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IIssueFilterOptions } from "types";
|
||||||
|
|
||||||
export const CURRENT_USER = "CURRENT_USER";
|
export const CURRENT_USER = "CURRENT_USER";
|
||||||
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
||||||
export const USER_WORKSPACES = "USER_WORKSPACES";
|
export const USER_WORKSPACES = "USER_WORKSPACES";
|
||||||
@ -24,6 +26,8 @@ export const PROJECT_INVITATIONS = "PROJECT_INVITATIONS";
|
|||||||
|
|
||||||
export const PROJECT_ISSUES_LIST = (workspaceSlug: string, projectId: string) =>
|
export const PROJECT_ISSUES_LIST = (workspaceSlug: string, projectId: string) =>
|
||||||
`PROJECT_ISSUES_LIST_${workspaceSlug}_${projectId}`;
|
`PROJECT_ISSUES_LIST_${workspaceSlug}_${projectId}`;
|
||||||
|
export const PROJECT_ISSUES_LIST_WITH_PARAMS = (projectId: string) =>
|
||||||
|
`PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId}`;
|
||||||
export const PROJECT_ISSUES_DETAILS = (issueId: string) => `PROJECT_ISSUES_DETAILS_${issueId}`;
|
export const PROJECT_ISSUES_DETAILS = (issueId: string) => `PROJECT_ISSUES_DETAILS_${issueId}`;
|
||||||
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
||||||
`PROJECT_ISSUES_PROPERTIES_${projectId}`;
|
`PROJECT_ISSUES_PROPERTIES_${projectId}`;
|
||||||
@ -36,6 +40,7 @@ export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
|
|||||||
|
|
||||||
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`;
|
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`;
|
||||||
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`;
|
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`;
|
||||||
|
export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string) => `CYCLE_ISSUES_WITH_PARAMS_${cycleId}`;
|
||||||
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId}`;
|
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId}`;
|
||||||
export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) =>
|
export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) =>
|
||||||
`CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId}`;
|
`CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId}`;
|
||||||
@ -50,6 +55,8 @@ export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${pro
|
|||||||
|
|
||||||
export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId}`;
|
export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId}`;
|
||||||
export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId}`;
|
export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId}`;
|
||||||
|
export const MODULE_ISSUES_WITH_PARAMS = (moduleId: string) =>
|
||||||
|
`MODULE_ISSUES_WITH_PARAMS_${moduleId}`;
|
||||||
export const MODULE_DETAILS = (moduleId: string) => `MODULE_DETAILS_${moduleId}`;
|
export const MODULE_DETAILS = (moduleId: string) => `MODULE_DETAILS_${moduleId}`;
|
||||||
|
|
||||||
export const VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId}`;
|
export const VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId}`;
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
// types
|
export const GROUP_BY_OPTIONS: Array<{
|
||||||
import { IIssue, NestedKeyOf } from "types";
|
name: string;
|
||||||
|
key: "state" | "priority" | "labels" | null;
|
||||||
export const GROUP_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
}> = [
|
||||||
{ name: "State", key: "state_detail.name" },
|
{ name: "State", key: "state" },
|
||||||
{ name: "Priority", key: "priority" },
|
{ name: "Priority", key: "priority" },
|
||||||
{ name: "Created By", key: "created_by" },
|
{ name: "Labels", key: "labels" },
|
||||||
{ name: "Assignee", key: "assignees" },
|
|
||||||
{ name: "None", key: null },
|
{ name: "None", key: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ORDER_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
export const ORDER_BY_OPTIONS: Array<{
|
||||||
|
name: string;
|
||||||
|
key: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||||
|
}> = [
|
||||||
{ name: "Manual", key: "sort_order" },
|
{ name: "Manual", key: "sort_order" },
|
||||||
{ name: "Last created", key: "created_at" },
|
{ name: "Last created", key: "created_at" },
|
||||||
{ name: "Last updated", key: "updated_at" },
|
{ name: "Last updated", key: "updated_at" },
|
||||||
@ -18,7 +20,7 @@ export const ORDER_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> |
|
|||||||
|
|
||||||
export const FILTER_ISSUE_OPTIONS: Array<{
|
export const FILTER_ISSUE_OPTIONS: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
key: "activeIssue" | "backlogIssue" | null;
|
key: "active" | "backlog" | null;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
name: "All",
|
name: "All",
|
||||||
@ -26,10 +28,78 @@ export const FILTER_ISSUE_OPTIONS: Array<{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Active Issues",
|
name: "Active Issues",
|
||||||
key: "activeIssue",
|
key: "active",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Backlog Issues",
|
name: "Backlog Issues",
|
||||||
key: "backlogIssue",
|
key: "backlog",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type THandleIssuesMutation = (
|
||||||
|
formData: Partial<IIssue>,
|
||||||
|
oldGroupTitle: string,
|
||||||
|
selectedGroupBy: "state" | "priority" | "labels" | null,
|
||||||
|
issueIndex: number,
|
||||||
|
prevData?:
|
||||||
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
| IIssue[]
|
||||||
|
) =>
|
||||||
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
| IIssue[]
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
export const handleIssuesMutation: THandleIssuesMutation = (
|
||||||
|
formData,
|
||||||
|
oldGroupTitle,
|
||||||
|
selectedGroupBy,
|
||||||
|
issueIndex,
|
||||||
|
prevData
|
||||||
|
) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
if (Array.isArray(prevData)) {
|
||||||
|
const updatedIssue = {
|
||||||
|
...prevData[issueIndex],
|
||||||
|
...formData,
|
||||||
|
assignees: formData?.assignees_list ?? prevData[issueIndex]?.assignees_list,
|
||||||
|
};
|
||||||
|
|
||||||
|
prevData.splice(issueIndex, 1, updatedIssue);
|
||||||
|
|
||||||
|
return [...prevData];
|
||||||
|
} else {
|
||||||
|
const oldGroup = prevData[oldGroupTitle ?? ""] ?? [];
|
||||||
|
|
||||||
|
let newGroup: IIssue[] = [];
|
||||||
|
|
||||||
|
if (selectedGroupBy === "priority") {
|
||||||
|
newGroup = prevData[formData.priority ?? ""] ?? [];
|
||||||
|
} else if (selectedGroupBy === "state") {
|
||||||
|
newGroup = prevData[formData.state ?? ""] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedIssue = {
|
||||||
|
...oldGroup[issueIndex],
|
||||||
|
...formData,
|
||||||
|
assignees: formData?.assignees_list ?? oldGroup[issueIndex]?.assignees_list,
|
||||||
|
};
|
||||||
|
|
||||||
|
oldGroup.splice(issueIndex, 1);
|
||||||
|
newGroup.push(updatedIssue);
|
||||||
|
|
||||||
|
const groupThatIsUpdated = selectedGroupBy === "priority" ? formData.priority : formData.state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
[oldGroupTitle ?? ""]: oldGroup,
|
||||||
|
[groupThatIsUpdated ?? ""]: newGroup,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -2,24 +2,29 @@ import { createContext, useCallback, useEffect, useReducer } from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import ToastAlert from "components/toast-alert";
|
import ToastAlert from "components/toast-alert";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// types
|
// types
|
||||||
import type { IIssue, NestedKeyOf } from "types";
|
import { IIssueFilterOptions, IProjectMember, NestedKeyOf } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
|
import {
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
USER_PROJECT_VIEW,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
export const issueViewContext = createContext<ContextType>({} as ContextType);
|
export const issueViewContext = createContext<ContextType>({} as ContextType);
|
||||||
|
|
||||||
type IssueViewProps = {
|
type IssueViewProps = {
|
||||||
issueView: "list" | "kanban" | null;
|
issueView: "list" | "kanban";
|
||||||
groupByProperty: NestedKeyOf<IIssue> | null;
|
groupByProperty: "state" | "priority" | "labels" | null;
|
||||||
filterIssue: "activeIssue" | "backlogIssue" | null;
|
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||||
orderBy: NestedKeyOf<IIssue> | null;
|
filters: IIssueFilterOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ReducerActionType = {
|
type ReducerActionType = {
|
||||||
@ -27,20 +32,16 @@ type ReducerActionType = {
|
|||||||
| "REHYDRATE_THEME"
|
| "REHYDRATE_THEME"
|
||||||
| "SET_ISSUE_VIEW"
|
| "SET_ISSUE_VIEW"
|
||||||
| "SET_ORDER_BY_PROPERTY"
|
| "SET_ORDER_BY_PROPERTY"
|
||||||
| "SET_FILTER_ISSUES"
|
| "SET_FILTERS"
|
||||||
| "SET_GROUP_BY_PROPERTY"
|
| "SET_GROUP_BY_PROPERTY"
|
||||||
| "RESET_TO_DEFAULT";
|
| "RESET_TO_DEFAULT";
|
||||||
payload?: Partial<IssueViewProps>;
|
payload?: Partial<IssueViewProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = IssueViewProps & {
|
||||||
orderBy: NestedKeyOf<IIssue> | null;
|
setGroupByProperty: (property: "state" | "priority" | "labels" | null) => void;
|
||||||
issueView: "list" | "kanban" | null;
|
setOrderBy: (property: "created_at" | "updated_at" | "priority" | "sort_order") => void;
|
||||||
groupByProperty: NestedKeyOf<IIssue> | null;
|
setFilters: (filters: Partial<IIssueFilterOptions>) => void;
|
||||||
filterIssue: "activeIssue" | "backlogIssue" | null;
|
|
||||||
setGroupByProperty: (property: NestedKeyOf<IIssue> | null) => void;
|
|
||||||
setOrderBy: (property: NestedKeyOf<IIssue> | null) => void;
|
|
||||||
setFilterIssue: (property: "activeIssue" | "backlogIssue" | null) => void;
|
|
||||||
resetFilterToDefault: () => void;
|
resetFilterToDefault: () => void;
|
||||||
setNewFilterDefaultView: () => void;
|
setNewFilterDefaultView: () => void;
|
||||||
setIssueViewToKanban: () => void;
|
setIssueViewToKanban: () => void;
|
||||||
@ -48,10 +49,10 @@ type ContextType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateType = {
|
type StateType = {
|
||||||
issueView: "list" | "kanban" | null;
|
issueView: "list" | "kanban";
|
||||||
groupByProperty: NestedKeyOf<IIssue> | null;
|
groupByProperty: "state" | "priority" | "labels" | null;
|
||||||
filterIssue: "activeIssue" | "backlogIssue" | null;
|
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||||
orderBy: NestedKeyOf<IIssue> | null;
|
filters: IIssueFilterOptions;
|
||||||
};
|
};
|
||||||
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
|
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
|
||||||
|
|
||||||
@ -59,7 +60,13 @@ export const initialState: StateType = {
|
|||||||
issueView: "list",
|
issueView: "list",
|
||||||
groupByProperty: null,
|
groupByProperty: null,
|
||||||
orderBy: "created_at",
|
orderBy: "created_at",
|
||||||
filterIssue: null,
|
filters: {
|
||||||
|
type: null,
|
||||||
|
assignees: null,
|
||||||
|
labels: null,
|
||||||
|
issue__assignees__id: null,
|
||||||
|
issue__labels__id: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer: ReducerFunctionType = (state, action) => {
|
export const reducer: ReducerFunctionType = (state, action) => {
|
||||||
@ -69,6 +76,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
case "REHYDRATE_THEME": {
|
case "REHYDRATE_THEME": {
|
||||||
let collapsed: any = localStorage.getItem("collapsed");
|
let collapsed: any = localStorage.getItem("collapsed");
|
||||||
collapsed = collapsed ? JSON.parse(collapsed) : false;
|
collapsed = collapsed ? JSON.parse(collapsed) : false;
|
||||||
|
|
||||||
return { ...initialState, ...payload, collapsed };
|
return { ...initialState, ...payload, collapsed };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +85,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
...state,
|
...state,
|
||||||
issueView: payload?.issueView || "list",
|
issueView: payload?.issueView || "list",
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...newState,
|
...newState,
|
||||||
@ -88,6 +97,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
...state,
|
...state,
|
||||||
groupByProperty: payload?.groupByProperty || null,
|
groupByProperty: payload?.groupByProperty || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...newState,
|
...newState,
|
||||||
@ -97,19 +107,24 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
case "SET_ORDER_BY_PROPERTY": {
|
case "SET_ORDER_BY_PROPERTY": {
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
orderBy: payload?.orderBy || null,
|
orderBy: payload?.orderBy || "created_at",
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...newState,
|
...newState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "SET_FILTER_ISSUES": {
|
case "SET_FILTERS": {
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
filterIssue: payload?.filterIssue || null,
|
filters: {
|
||||||
|
...state.filters,
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...newState,
|
...newState,
|
||||||
@ -135,8 +150,21 @@ const saveDataToServer = async (workspaceSlug: string, projectID: string, state:
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNewDefault = async (workspaceSlug: string, projectID: string, state: any) => {
|
const setNewDefault = async (workspaceSlug: string, projectId: string, state: any) => {
|
||||||
await projectService.setProjectView(workspaceSlug, projectID, {
|
mutate<IProjectMember>(
|
||||||
|
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
view_props: state,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await projectService.setProjectView(workspaceSlug, projectId, {
|
||||||
view_props: state,
|
view_props: state,
|
||||||
default_props: state,
|
default_props: state,
|
||||||
});
|
});
|
||||||
@ -146,7 +174,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
|
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
|
||||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
||||||
@ -162,10 +190,11 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
issueView: "kanban",
|
issueView: "kanban",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_GROUP_BY_PROPERTY",
|
type: "SET_GROUP_BY_PROPERTY",
|
||||||
payload: {
|
payload: {
|
||||||
groupByProperty: "state_detail.name",
|
groupByProperty: "state",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -174,7 +203,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
saveDataToServer(workspaceSlug as string, projectId as string, {
|
saveDataToServer(workspaceSlug as string, projectId as string, {
|
||||||
...state,
|
...state,
|
||||||
issueView: "kanban",
|
issueView: "kanban",
|
||||||
groupByProperty: "state_detail.name",
|
groupByProperty: "state",
|
||||||
});
|
});
|
||||||
}, [workspaceSlug, projectId, state]);
|
}, [workspaceSlug, projectId, state]);
|
||||||
|
|
||||||
@ -185,6 +214,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
issueView: "list",
|
issueView: "list",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_GROUP_BY_PROPERTY",
|
type: "SET_GROUP_BY_PROPERTY",
|
||||||
payload: {
|
payload: {
|
||||||
@ -194,15 +224,28 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
mutateMyViewProps((prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
view_props: {
|
||||||
|
...state,
|
||||||
|
issueView: "list",
|
||||||
|
groupByProperty: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, false);
|
||||||
|
|
||||||
saveDataToServer(workspaceSlug as string, projectId as string, {
|
saveDataToServer(workspaceSlug as string, projectId as string, {
|
||||||
...state,
|
...state,
|
||||||
issueView: "list",
|
issueView: "list",
|
||||||
groupByProperty: null,
|
groupByProperty: null,
|
||||||
});
|
});
|
||||||
}, [workspaceSlug, projectId, state]);
|
}, [workspaceSlug, projectId, state, mutateMyViewProps]);
|
||||||
|
|
||||||
const setGroupByProperty = useCallback(
|
const setGroupByProperty = useCallback(
|
||||||
(property: NestedKeyOf<IIssue> | null) => {
|
(property: "state" | "priority" | "labels" | null) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_GROUP_BY_PROPERTY",
|
type: "SET_GROUP_BY_PROPERTY",
|
||||||
payload: {
|
payload: {
|
||||||
@ -211,16 +254,29 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
mutateMyViewProps((prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
view_props: {
|
||||||
|
...state,
|
||||||
|
groupByProperty: property,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, false);
|
||||||
|
|
||||||
saveDataToServer(workspaceSlug as string, projectId as string, {
|
saveDataToServer(workspaceSlug as string, projectId as string, {
|
||||||
...state,
|
...state,
|
||||||
groupByProperty: property,
|
groupByProperty: property,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[projectId, workspaceSlug, state]
|
[projectId, workspaceSlug, state, mutateMyViewProps]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setOrderBy = useCallback(
|
const setOrderBy = useCallback(
|
||||||
(property: NestedKeyOf<IIssue> | null) => {
|
(property: "created_at" | "updated_at" | "priority" | "sort_order") => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_ORDER_BY_PROPERTY",
|
type: "SET_ORDER_BY_PROPERTY",
|
||||||
payload: {
|
payload: {
|
||||||
@ -229,34 +285,70 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
mutateMyViewProps((prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
view_props: {
|
||||||
|
...state,
|
||||||
|
orderBy: property,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, false);
|
||||||
|
|
||||||
saveDataToServer(workspaceSlug as string, projectId as string, {
|
saveDataToServer(workspaceSlug as string, projectId as string, {
|
||||||
...state,
|
...state,
|
||||||
orderBy: property,
|
orderBy: property,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[projectId, workspaceSlug, state]
|
[projectId, workspaceSlug, state, mutateMyViewProps]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setFilterIssue = useCallback(
|
const setFilters = useCallback(
|
||||||
(property: "activeIssue" | "backlogIssue" | null) => {
|
(property: Partial<IIssueFilterOptions>) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_FILTER_ISSUES",
|
type: "SET_FILTERS",
|
||||||
payload: {
|
payload: {
|
||||||
filterIssue: property,
|
filters: {
|
||||||
|
...state.filters,
|
||||||
|
...property,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
mutateMyViewProps((prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
view_props: {
|
||||||
|
...state,
|
||||||
|
filters: {
|
||||||
|
...state.filters,
|
||||||
|
...property,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, false);
|
||||||
|
|
||||||
saveDataToServer(workspaceSlug as string, projectId as string, {
|
saveDataToServer(workspaceSlug as string, projectId as string, {
|
||||||
...state,
|
...state,
|
||||||
filterIssue: property,
|
filters: {
|
||||||
|
...state.filters,
|
||||||
|
...property,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[projectId, workspaceSlug, state]
|
[projectId, workspaceSlug, state, mutateMyViewProps]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setNewDefaultView = useCallback(() => {
|
const setNewDefaultView = useCallback(() => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
setNewDefault(workspaceSlug as string, projectId as string, state).then(() => {
|
setNewDefault(workspaceSlug as string, projectId as string, state).then(() => {
|
||||||
mutateMyViewProps();
|
mutateMyViewProps();
|
||||||
});
|
});
|
||||||
@ -267,7 +359,9 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
type: "RESET_TO_DEFAULT",
|
type: "RESET_TO_DEFAULT",
|
||||||
payload: myViewProps?.default_props,
|
payload: myViewProps?.default_props,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
saveDataToServer(workspaceSlug as string, projectId as string, myViewProps?.default_props);
|
saveDataToServer(workspaceSlug as string, projectId as string, myViewProps?.default_props);
|
||||||
}, [projectId, workspaceSlug, myViewProps]);
|
}, [projectId, workspaceSlug, myViewProps]);
|
||||||
|
|
||||||
@ -278,6 +372,20 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
});
|
});
|
||||||
}, [myViewProps]);
|
}, [myViewProps]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: think of a better way to do this
|
||||||
|
if (cycleId) {
|
||||||
|
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string), {}, false);
|
||||||
|
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string));
|
||||||
|
} else if (moduleId) {
|
||||||
|
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string), {}, false);
|
||||||
|
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string));
|
||||||
|
} else {
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string), {}, false);
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
|
||||||
|
}
|
||||||
|
}, [state, projectId, cycleId, moduleId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<issueViewContext.Provider
|
<issueViewContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@ -286,8 +394,8 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
setGroupByProperty,
|
setGroupByProperty,
|
||||||
orderBy: state.orderBy,
|
orderBy: state.orderBy,
|
||||||
setOrderBy,
|
setOrderBy,
|
||||||
filterIssue: state.filterIssue,
|
filters: state.filters,
|
||||||
setFilterIssue,
|
setFilters,
|
||||||
resetFilterToDefault: resetToDefault,
|
resetFilterToDefault: resetToDefault,
|
||||||
setNewFilterDefaultView: setNewDefaultView,
|
setNewFilterDefaultView: setNewDefaultView,
|
||||||
setIssueViewToKanban,
|
setIssueViewToKanban,
|
||||||
|
@ -81,7 +81,7 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "REHYDRATE_THEME",
|
type: "REHYDRATE_THEME",
|
||||||
payload: myViewProps?.view_props,
|
payload: myViewProps?.view_props as any,
|
||||||
});
|
});
|
||||||
}, [myViewProps]);
|
}, [myViewProps]);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const debounce = (func: any, wait: number, immediate: boolean = false) => {
|
export const debounce = (func: any, wait: number, immediate: boolean = false) => {
|
||||||
let timeout: any;
|
let timeout: any;
|
||||||
|
|
||||||
return function executedFunction(...args: any) {
|
return function executedFunction(...args: any) {
|
||||||
const later = () => {
|
const later = () => {
|
||||||
timeout = null;
|
timeout = null;
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// services
|
|
||||||
import stateService from "services/state.service";
|
|
||||||
// contexts
|
|
||||||
import { issueViewContext } from "contexts/issue-view.context";
|
|
||||||
// helpers
|
|
||||||
import { groupBy, orderArrayBy } from "helpers/array.helper";
|
|
||||||
import { getStatesList } from "helpers/state.helper";
|
|
||||||
// types
|
|
||||||
import { IIssue, IState } from "types";
|
|
||||||
// fetch-keys
|
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
|
||||||
// constants
|
|
||||||
import { PRIORITIES } from "constants/project";
|
|
||||||
|
|
||||||
const useIssueView = (projectIssues: IIssue[]) => {
|
|
||||||
const {
|
|
||||||
issueView,
|
|
||||||
groupByProperty,
|
|
||||||
setGroupByProperty,
|
|
||||||
orderBy,
|
|
||||||
setOrderBy,
|
|
||||||
filterIssue,
|
|
||||||
setFilterIssue,
|
|
||||||
resetFilterToDefault,
|
|
||||||
setNewFilterDefaultView,
|
|
||||||
setIssueViewToKanban,
|
|
||||||
setIssueViewToList,
|
|
||||||
} = useContext(issueViewContext);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
const states = getStatesList(stateGroups ?? {});
|
|
||||||
|
|
||||||
let groupedByIssues: {
|
|
||||||
[key: string]: IIssue[];
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
const groupIssues = (states: IState[], issues: IIssue[]) => ({
|
|
||||||
...(groupByProperty === "state_detail.name"
|
|
||||||
? Object.fromEntries(
|
|
||||||
states
|
|
||||||
?.sort((a, b) => a.sequence - b.sequence)
|
|
||||||
?.map((state) => [
|
|
||||||
state.name,
|
|
||||||
issues.filter((issue) => issue.state === state.name) ?? [],
|
|
||||||
]) ?? []
|
|
||||||
)
|
|
||||||
: groupByProperty === "priority"
|
|
||||||
? Object.fromEntries(
|
|
||||||
PRIORITIES.map((priority) => [
|
|
||||||
priority,
|
|
||||||
issues.filter((issue) => issue.priority === priority) ?? [],
|
|
||||||
])
|
|
||||||
)
|
|
||||||
: {}),
|
|
||||||
...groupBy(issues ?? [], groupByProperty ?? ""),
|
|
||||||
});
|
|
||||||
|
|
||||||
groupedByIssues = groupIssues(states ?? [], projectIssues);
|
|
||||||
|
|
||||||
if (filterIssue) {
|
|
||||||
if (filterIssue === "activeIssue") {
|
|
||||||
const filteredStates = states?.filter(
|
|
||||||
(s) => s.group === "started" || s.group === "unstarted"
|
|
||||||
);
|
|
||||||
const filteredIssues = projectIssues.filter(
|
|
||||||
(i) => i.state_detail.group === "started" || i.state_detail.group === "unstarted"
|
|
||||||
);
|
|
||||||
|
|
||||||
groupedByIssues = groupIssues(filteredStates ?? [], filteredIssues);
|
|
||||||
} else if (filterIssue === "backlogIssue") {
|
|
||||||
const filteredStates = states?.filter(
|
|
||||||
(s) => s.group === "backlog" || s.group === "cancelled"
|
|
||||||
);
|
|
||||||
const filteredIssues = projectIssues.filter(
|
|
||||||
(i) => i.state_detail.group === "backlog" || i.state_detail.group === "cancelled"
|
|
||||||
);
|
|
||||||
|
|
||||||
groupedByIssues = groupIssues(filteredStates ?? [], filteredIssues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderBy) {
|
|
||||||
groupedByIssues = Object.fromEntries(
|
|
||||||
Object.entries(groupedByIssues).map(([key, value]) => [
|
|
||||||
key,
|
|
||||||
orderArrayBy(value, orderBy, orderBy === "sort_order" ? "ascending" : "descending"),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupByProperty === "priority") {
|
|
||||||
delete groupedByIssues.None;
|
|
||||||
if (orderBy === "priority") setOrderBy("created_at");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
groupedByIssues,
|
|
||||||
issueView,
|
|
||||||
groupByProperty,
|
|
||||||
setGroupByProperty,
|
|
||||||
orderBy,
|
|
||||||
setOrderBy,
|
|
||||||
filterIssue,
|
|
||||||
setFilterIssue,
|
|
||||||
resetFilterToDefault,
|
|
||||||
setNewFilterDefaultView,
|
|
||||||
setIssueViewToKanban,
|
|
||||||
setIssueViewToList,
|
|
||||||
} as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useIssueView;
|
|
121
apps/app/hooks/use-issues-view.tsx
Normal file
121
apps/app/hooks/use-issues-view.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// contexts
|
||||||
|
import { issueViewContext } from "contexts/issue-view.context";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
import cyclesService from "services/cycles.service";
|
||||||
|
import modulesService from "services/modules.service";
|
||||||
|
// fetch-keys
|
||||||
|
import {
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import type { IIssue } from "types";
|
||||||
|
|
||||||
|
const useIssuesView = () => {
|
||||||
|
const {
|
||||||
|
issueView,
|
||||||
|
groupByProperty,
|
||||||
|
setGroupByProperty,
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
resetFilterToDefault,
|
||||||
|
setNewFilterDefaultView,
|
||||||
|
setIssueViewToKanban,
|
||||||
|
setIssueViewToList,
|
||||||
|
} = useContext(issueViewContext);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
order_by: orderBy,
|
||||||
|
group_by: groupByProperty,
|
||||||
|
assignees: filters.assignees ? filters.assignees.join(",") : undefined,
|
||||||
|
type: filters.type ? filters.type : undefined,
|
||||||
|
labels: filters.labels ? filters.labels.join(",") : undefined,
|
||||||
|
issue__assignees__id: filters.issue__assignees__id
|
||||||
|
? filters.issue__assignees__id.join(",")
|
||||||
|
: undefined,
|
||||||
|
issue__labels__id: filters.issue__labels__id ? filters.issue__labels__id.join(",") : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: projectIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId && params
|
||||||
|
? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && params
|
||||||
|
? () =>
|
||||||
|
issuesService.getIssuesWithParams(workspaceSlug as string, projectId as string, params)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: cycleIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId && cycleId && params
|
||||||
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && cycleId && params
|
||||||
|
? () =>
|
||||||
|
cyclesService.getCycleIssuesWithParams(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
cycleId as string,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: moduleIssues } = useSWR(
|
||||||
|
workspaceSlug && projectId && moduleId && params
|
||||||
|
? MODULE_ISSUES_WITH_PARAMS(moduleId as string)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && moduleId && params
|
||||||
|
? () =>
|
||||||
|
modulesService.getModuleIssuesWithParams(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
moduleId as string,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedByIssues:
|
||||||
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
| undefined = useMemo(() => {
|
||||||
|
const issuesToGroup = cycleIssues ?? moduleIssues ?? projectIssues;
|
||||||
|
|
||||||
|
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
||||||
|
else return issuesToGroup;
|
||||||
|
}, [projectIssues, cycleIssues, moduleIssues]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupedByIssues,
|
||||||
|
issueView,
|
||||||
|
groupByProperty,
|
||||||
|
setGroupByProperty,
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
params,
|
||||||
|
resetFilterToDefault,
|
||||||
|
setNewFilterDefaultView,
|
||||||
|
setIssueViewToKanban,
|
||||||
|
setIssueViewToList,
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIssuesView;
|
@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowLeftIcon, ListBulletIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||||
import { CyclesIcon } from "components/icons";
|
import { CyclesIcon } from "components/icons";
|
||||||
// lib
|
// lib
|
||||||
import { requiredAdmin, requiredAuth } from "lib/auth";
|
import { requiredAdmin, requiredAuth } from "lib/auth";
|
||||||
@ -21,21 +21,15 @@ import issuesServices from "services/issues.service";
|
|||||||
import cycleServices from "services/cycles.service";
|
import cycleServices from "services/cycles.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { CycleIssueResponse, UserAuth } from "types";
|
import { UserAuth } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import { CYCLE_ISSUES, CYCLE_LIST, PROJECT_DETAILS, CYCLE_DETAILS } from "constants/fetch-keys";
|
||||||
CYCLE_ISSUES,
|
|
||||||
CYCLE_LIST,
|
|
||||||
PROJECT_ISSUES_LIST,
|
|
||||||
PROJECT_DETAILS,
|
|
||||||
CYCLE_DETAILS,
|
|
||||||
} from "constants/fetch-keys";
|
|
||||||
|
|
||||||
const SingleCycle: React.FC<UserAuth> = (props) => {
|
const SingleCycle: React.FC<UserAuth> = (props) => {
|
||||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||||
@ -51,15 +45,6 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: issues } = useSWR(
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: cycles } = useSWR(
|
const { data: cycles } = useSWR(
|
||||||
workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
@ -84,7 +69,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
|||||||
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
|
const { data: issues } = useSWR(
|
||||||
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
|
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
|
||||||
workspaceSlug && projectId && cycleId
|
workspaceSlug && projectId && cycleId
|
||||||
? () =>
|
? () =>
|
||||||
@ -96,13 +81,6 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const cycleIssuesArray = cycleIssues?.map((issue) => ({
|
|
||||||
...issue.issue_detail,
|
|
||||||
sub_issues_count: issue.sub_issues_count,
|
|
||||||
bridge: issue.id,
|
|
||||||
cycle: cycleId as string,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const openIssuesListModal = () => {
|
const openIssuesListModal = () => {
|
||||||
setCycleIssuesListModal(true);
|
setCycleIssuesListModal(true);
|
||||||
};
|
};
|
||||||
@ -164,7 +142,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
|||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}
|
className={`flex items-center gap-2 ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}
|
||||||
>
|
>
|
||||||
<IssuesFilterView issues={cycleIssuesArray ?? []} />
|
<IssuesFilterView />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100 ${
|
||||||
@ -177,59 +155,10 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{cycleIssuesArray ? (
|
<div className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}>
|
||||||
cycleIssuesArray.length > 0 ? (
|
<IssuesView type="cycle" userAuth={props} openIssuesListModal={openIssuesListModal} />
|
||||||
<div className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}>
|
</div>
|
||||||
<IssuesView
|
<CycleDetailsSidebar cycleStatus={cycleStatus} cycle={cycleDetails} isOpen={cycleSidebar} />
|
||||||
type="cycle"
|
|
||||||
issues={cycleIssuesArray ?? []}
|
|
||||||
userAuth={props}
|
|
||||||
openIssuesListModal={openIssuesListModal}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`flex h-full flex-col items-center justify-center px-4 ${
|
|
||||||
cycleSidebar ? "mr-[24rem]" : ""
|
|
||||||
} duration-300`}
|
|
||||||
>
|
|
||||||
<EmptySpace
|
|
||||||
title="You don't have any issue yet."
|
|
||||||
description="A cycle is a fixed time period where a team commits to a set number of issues from their backlog. Cycles are usually one, two, or four weeks long."
|
|
||||||
Icon={CyclesIcon}
|
|
||||||
>
|
|
||||||
<EmptySpaceItem
|
|
||||||
title="Create a new issue"
|
|
||||||
description="Click to create a new issue inside the cycle."
|
|
||||||
Icon={PlusIcon}
|
|
||||||
action={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", {
|
|
||||||
key: "c",
|
|
||||||
});
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<EmptySpaceItem
|
|
||||||
title="Add an existing issue"
|
|
||||||
description="Open list"
|
|
||||||
Icon={ListBulletIcon}
|
|
||||||
action={openIssuesListModal}
|
|
||||||
/>
|
|
||||||
</EmptySpace>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<CycleDetailsSidebar
|
|
||||||
cycleStatus={cycleStatus}
|
|
||||||
issues={cycleIssuesArray ?? []}
|
|
||||||
cycle={cycleDetails}
|
|
||||||
isOpen={cycleSidebar}
|
|
||||||
cycleIssues={cycleIssues ?? []}
|
|
||||||
/>
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</IssueViewContextProvider>
|
</IssueViewContextProvider>
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,6 @@ import useSWR from "swr";
|
|||||||
// lib
|
// lib
|
||||||
import { requiredAdmin, requiredAuth } from "lib/auth";
|
import { requiredAdmin, requiredAuth } from "lib/auth";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "services/issues.service";
|
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// layouts
|
// layouts
|
||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
@ -14,31 +13,20 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
|
|||||||
// components
|
// components
|
||||||
import { IssuesFilterView, IssuesView } from "components/core";
|
import { IssuesFilterView, IssuesView } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, EmptySpace, EmptySpaceItem, HeaderButton, EmptyState } from "components/ui";
|
import { HeaderButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { RectangleStackIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { UserAuth } from "types";
|
import type { UserAuth } from "types";
|
||||||
import type { GetServerSidePropsContext, NextPage } from "next";
|
import type { GetServerSidePropsContext, NextPage } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||||
// image
|
|
||||||
import emptyIssue from "public/empty-state/empty-issue.svg";
|
|
||||||
|
|
||||||
const ProjectIssues: NextPage<UserAuth> = (props) => {
|
const ProjectIssues: NextPage<UserAuth> = (props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { data: projectIssues } = useSWR(
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR(
|
const { data: projectDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
@ -57,7 +45,7 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
|
|||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<IssuesFilterView issues={projectIssues?.filter((p) => p.parent === null) ?? []} />
|
<IssuesFilterView />
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
Icon={PlusIcon}
|
Icon={PlusIcon}
|
||||||
label="Add Issue"
|
label="Add Issue"
|
||||||
@ -71,26 +59,7 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{projectIssues ? (
|
<IssuesView userAuth={props} />
|
||||||
projectIssues.length > 0 ? (
|
|
||||||
<IssuesView
|
|
||||||
issues={projectIssues?.filter((p) => p.parent === null) ?? []}
|
|
||||||
userAuth={props}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EmptyState
|
|
||||||
type="issue"
|
|
||||||
title="Create New Issue"
|
|
||||||
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done.
|
|
||||||
Create a new issue"
|
|
||||||
imgURL={emptyIssue}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</IssueViewContextProvider>
|
</IssueViewContextProvider>
|
||||||
);
|
);
|
||||||
|
@ -30,7 +30,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IModule, ModuleIssueResponse, UserAuth } from "types";
|
import { IModule, UserAuth } from "types";
|
||||||
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
@ -63,7 +63,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: moduleIssues } = useSWR<ModuleIssueResponse[]>(
|
const { data: moduleIssues } = useSWR(
|
||||||
workspaceSlug && projectId && moduleId ? MODULE_ISSUES(moduleId as string) : null,
|
workspaceSlug && projectId && moduleId ? MODULE_ISSUES(moduleId as string) : null,
|
||||||
workspaceSlug && projectId && moduleId
|
workspaceSlug && projectId && moduleId
|
||||||
? () =>
|
? () =>
|
||||||
@ -87,13 +87,6 @@ const SingleModule: React.FC<UserAuth> = (props) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const moduleIssuesArray = moduleIssues?.map((issue) => ({
|
|
||||||
...issue.issue_detail,
|
|
||||||
sub_issues_count: issue.sub_issues_count,
|
|
||||||
bridge: issue.id,
|
|
||||||
module: moduleId as string,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const handleAddIssuesToModule = async (data: { issues: string[] }) => {
|
const handleAddIssuesToModule = async (data: { issues: string[] }) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
@ -153,7 +146,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
|
|||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 ${moduleSidebar ? "mr-[24rem]" : ""} duration-300`}
|
className={`flex items-center gap-2 ${moduleSidebar ? "mr-[24rem]" : ""} duration-300`}
|
||||||
>
|
>
|
||||||
<IssuesFilterView issues={moduleIssuesArray ?? []} />
|
<IssuesFilterView />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100 ${
|
||||||
@ -166,12 +159,11 @@ const SingleModule: React.FC<UserAuth> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{moduleIssuesArray ? (
|
{moduleIssues ? (
|
||||||
moduleIssuesArray.length > 0 ? (
|
moduleIssues.length > 0 ? (
|
||||||
<div className={`h-full ${moduleSidebar ? "mr-[24rem]" : ""} duration-300`}>
|
<div className={`h-full ${moduleSidebar ? "mr-[24rem]" : ""} duration-300`}>
|
||||||
<IssuesView
|
<IssuesView
|
||||||
type="module"
|
type="module"
|
||||||
issues={moduleIssuesArray ?? []}
|
|
||||||
userAuth={props}
|
userAuth={props}
|
||||||
openIssuesListModal={openIssuesListModal}
|
openIssuesListModal={openIssuesListModal}
|
||||||
/>
|
/>
|
||||||
@ -213,7 +205,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ModuleDetailsSidebar
|
<ModuleDetailsSidebar
|
||||||
issues={moduleIssuesArray ?? []}
|
issues={moduleIssues ?? []}
|
||||||
module={moduleDetails}
|
module={moduleDetails}
|
||||||
isOpen={moduleSidebar}
|
isOpen={moduleSidebar}
|
||||||
moduleIssues={moduleIssues}
|
moduleIssues={moduleIssues}
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
// types
|
// types
|
||||||
import type { CompletedCyclesResponse, CurrentAndUpcomingCyclesResponse, DraftCyclesResponse, ICycle } from "types";
|
import type {
|
||||||
|
CycleIssueResponse,
|
||||||
|
CompletedCyclesResponse,
|
||||||
|
CurrentAndUpcomingCyclesResponse,
|
||||||
|
DraftCyclesResponse,
|
||||||
|
ICycle,
|
||||||
|
IIssue,
|
||||||
|
IIssueViewOptions,
|
||||||
|
} from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
@ -38,7 +46,11 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCycleIssues(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
async getCycleIssues(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string
|
||||||
|
): Promise<IIssue[]> {
|
||||||
return this.get(
|
return this.get(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`
|
||||||
)
|
)
|
||||||
@ -48,6 +60,22 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCycleIssuesWithParams(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
queries?: IIssueViewOptions
|
||||||
|
): Promise<IIssue[] | { [key: string]: IIssue[] }> {
|
||||||
|
return this.get(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`,
|
||||||
|
{ params: queries }
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateCycle(
|
async updateCycle(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -88,18 +116,28 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async cycleDateCheck(workspaceSlug: string, projectId: string, data: {
|
async cycleDateCheck(
|
||||||
start_date: string,
|
workspaceSlug: string,
|
||||||
end_date: string
|
projectId: string,
|
||||||
}): Promise<any> {
|
data: {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data)
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
}
|
||||||
|
): Promise<any> {
|
||||||
|
return this.post(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`,
|
||||||
|
data
|
||||||
|
)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise<CurrentAndUpcomingCyclesResponse> {
|
async getCurrentAndUpcomingCycles(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string
|
||||||
|
): Promise<CurrentAndUpcomingCyclesResponse> {
|
||||||
return this.get(
|
return this.get(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/`
|
||||||
)
|
)
|
||||||
@ -110,16 +148,17 @@ class ProjectCycleServices extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDraftCycles(workspaceSlug: string, projectId: string): Promise<DraftCyclesResponse> {
|
async getDraftCycles(workspaceSlug: string, projectId: string): Promise<DraftCyclesResponse> {
|
||||||
return this.get(
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`)
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`
|
|
||||||
)
|
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletedCycles(workspaceSlug: string, projectId: string): Promise<CompletedCyclesResponse> {
|
async getCompletedCycles(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string
|
||||||
|
): Promise<CompletedCyclesResponse> {
|
||||||
return this.get(
|
return this.get(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/`
|
||||||
)
|
)
|
||||||
@ -136,21 +175,29 @@ class ProjectCycleServices extends APIService {
|
|||||||
cycle: string;
|
cycle: string;
|
||||||
}
|
}
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/`, data)
|
return this.post(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/`,
|
||||||
|
data
|
||||||
|
)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeCycleFromFavorites(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
async removeCycleFromFavorites(
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/${cycleId}/`)
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string
|
||||||
|
): Promise<any> {
|
||||||
|
return this.delete(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/${cycleId}/`
|
||||||
|
)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ProjectCycleServices();
|
export default new ProjectCycleServices();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
// type
|
// type
|
||||||
import type { IIssue, IIssueActivity, IIssueComment } from "types";
|
import type { IIssue, IIssueActivity, IIssueComment, IIssueViewOptions } from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
@ -26,6 +26,20 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getIssuesWithParams(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
queries?: any
|
||||||
|
): Promise<IIssue[] | { [key: string]: IIssue[] }> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, {
|
||||||
|
params: queries,
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
// types
|
// types
|
||||||
import type { IModule } from "types";
|
import type { IIssueViewOptions, IModule, ModuleIssueResponse, IIssue } from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
@ -76,7 +76,11 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getModuleIssues(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
|
async getModuleIssues(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string
|
||||||
|
): Promise<IIssue[]> {
|
||||||
return this.get(
|
return this.get(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`
|
||||||
)
|
)
|
||||||
@ -86,6 +90,27 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getModuleIssuesWithParams(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string,
|
||||||
|
queries?: IIssueViewOptions
|
||||||
|
): Promise<
|
||||||
|
| IIssue[]
|
||||||
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
return this.get(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`,
|
||||||
|
{ params: queries }
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async addIssuesToModule(
|
async addIssuesToModule(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -159,15 +184,24 @@ class ProjectIssuesServices extends APIService {
|
|||||||
module: string;
|
module: string;
|
||||||
}
|
}
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/`, data)
|
return this.post(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/`,
|
||||||
|
data
|
||||||
|
)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeModuleFromFavorites(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
|
async removeModuleFromFavorites(
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/${moduleId}/`)
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string
|
||||||
|
): Promise<any> {
|
||||||
|
return this.delete(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/${moduleId}/`
|
||||||
|
)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
|
16
apps/app/types/issues.d.ts
vendored
16
apps/app/types/issues.d.ts
vendored
@ -67,7 +67,7 @@ export interface IIssue {
|
|||||||
blockers: any[];
|
blockers: any[];
|
||||||
blockers_list: string[];
|
blockers_list: string[];
|
||||||
blocks_list: string[];
|
blocks_list: string[];
|
||||||
bridge: string;
|
bridge_id?: string | null;
|
||||||
completed_at: Date;
|
completed_at: Date;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
@ -206,3 +206,17 @@ export interface IIssueActivity {
|
|||||||
issue_comment: string | null;
|
issue_comment: string | null;
|
||||||
actor: string;
|
actor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IIssueFilterOptions {
|
||||||
|
type: "active" | "backlog" | null;
|
||||||
|
assignees: string[] | null;
|
||||||
|
labels: string[] | null;
|
||||||
|
issue__assignees__id: string[] | null;
|
||||||
|
issue__labels__id: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIssueViewOptions {
|
||||||
|
group_by: "state" | "priority" | "labels" | null;
|
||||||
|
order_by: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||||
|
filters: IIssueFilterOptions;
|
||||||
|
}
|
||||||
|
11
apps/app/types/projects.d.ts
vendored
11
apps/app/types/projects.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import type { IUserLite, IWorkspace } from "./";
|
import type { IIssueFilterOptions, IUserLite, IWorkspace } from "./";
|
||||||
|
|
||||||
export interface IProject {
|
export interface IProject {
|
||||||
cover_image: string | null;
|
cover_image: string | null;
|
||||||
@ -34,11 +34,10 @@ export interface IFavoriteProject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProjectViewTheme = {
|
type ProjectViewTheme = {
|
||||||
collapsed: boolean;
|
issueView: "list" | "kanban";
|
||||||
issueView: "list" | "kanban" | null;
|
groupByProperty: "state" | "priority" | "labels" | null;
|
||||||
groupByProperty: NestedKeyOf<IIssue> | null;
|
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||||
filterIssue: "activeIssue" | "backlogIssue" | null;
|
filters: IIssueFilterOptions;
|
||||||
orderBy: NestedKeyOf<IIssue> | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IProjectMember {
|
export interface IProjectMember {
|
||||||
|
Loading…
Reference in New Issue
Block a user