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:
Dakshesh Jain 2023-03-15 11:44:44 +05:30 committed by GitHub
parent 636e8e6c60
commit 928ebdf632
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1149 additions and 1036 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
) : ( ) : (
"" ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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