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
import useIssueView from "hooks/use-issue-view";
import useProjectIssuesView from "hooks/use-issues-view";
// components
import { SingleBoard } from "components/core/board-view/single-board";
// types
import { IIssue, IProjectMember, IState, UserAuth } from "types";
import { IIssue, IState, UserAuth } from "types";
type Props = {
type: "issue" | "cycle" | "module";
issues: IIssue[];
states: IState[] | undefined;
members: IProjectMember[] | undefined;
addIssueToState: (groupTitle: string, stateId: string | null) => void;
addIssueToState: (groupTitle: string) => void;
makeIssueCopy: (issue: IIssue) => void;
handleEditIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
@ -22,9 +20,7 @@ type Props = {
export const AllBoards: React.FC<Props> = ({
type,
issues,
states,
members,
addIssueToState,
makeIssueCopy,
handleEditIssue,
@ -34,56 +30,35 @@ export const AllBoards: React.FC<Props> = ({
removeIssue,
userAuth,
}) => {
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssueView(issues);
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useProjectIssuesView();
return (
<>
{groupedByIssues ? (
<div className="h-[calc(100vh-140px)] w-full">
<div className="horizontal-scroll-enable flex h-full gap-x-3.5 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)
: null;
<div className="horizontal-scroll-enable flex h-[calc(100vh-140px)] gap-x-4">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : 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 (
<SingleBoard
key={index}
type={type}
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>
return (
<SingleBoard
key={index}
type={type}
currentState={currentState}
groupTitle={singleGroup}
handleEditIssue={handleEditIssue}
makeIssueCopy={makeIssueCopy}
addIssueToState={() => addIssueToState(singleGroup)}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={openIssuesListModal ?? null}
handleTrashBox={handleTrashBox}
removeIssue={removeIssue}
userAuth={userAuth}
/>
);
})}
</div>
) : (
<div className="flex h-full w-full items-center justify-center">Loading...</div>
)}
) : null}
</>
);
};

View File

@ -1,52 +1,42 @@
import React from "react";
// hooks
import useIssuesView from "hooks/use-issues-view";
// icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IProjectMember, IState, NestedKeyOf } from "types";
import { getStateGroupIcon } from "components/icons";
import { IState } from "types";
type Props = {
groupedByIssues: {
[key: string]: IIssue[];
};
currentState?: IState | null;
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string;
bgColor?: string;
addIssueToState: () => void;
members: IProjectMember[] | undefined;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
};
export const BoardHeader: React.FC<Props> = ({
groupedByIssues,
currentState,
selectedGroup,
groupTitle,
bgColor,
addIssueToState,
isCollapsed,
setIsCollapsed,
members,
}) => {
const createdBy =
selectedGroup === "created_by"
? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..."
: null;
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
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";
}
let bgColor = "#000000";
if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000";
if (selectedGroup === "priority")
groupTitle === "high"
? (bgColor = "#dc2626")
: groupTitle === "medium"
? (bgColor = "#f97316")
: groupTitle === "low"
? (bgColor = "#22c55e")
: (bgColor = "#ff0000");
return (
<div
@ -67,14 +57,12 @@ export const BoardHeader: React.FC<Props> = ({
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}
>
{selectedGroup === "created_by"
? createdBy
: selectedGroup === "assignees"
? assignees
{selectedGroup === "state"
? addSpaceIfCamelCase(currentState?.name ?? "")
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="ml-0.5 rounded-full bg-gray-100 py-1 px-3 text-sm">
{groupedByIssues[groupTitle].length}
{groupedByIssues?.[groupTitle].length ?? 0}
</span>
</div>
</div>

View File

@ -6,6 +6,7 @@ import { useRouter } from "next/router";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable } from "react-beautiful-dnd";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useIssuesProperties from "hooks/use-issue-properties";
// components
import { BoardHeader, SingleBoardIssue } from "components/core";
@ -16,24 +17,17 @@ import { PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
import { IIssue, IState, UserAuth } from "types";
type Props = {
type?: "issue" | "cycle" | "module";
currentState?: IState | null;
bgColor?: string;
groupTitle: string;
groupedByIssues: {
[key: string]: IIssue[];
};
selectedGroup: NestedKeyOf<IIssue> | null;
members: IProjectMember[] | undefined;
handleEditIssue: (issue: IIssue) => void;
makeIssueCopy: (issue: IIssue) => void;
addIssueToState: () => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
orderBy: NestedKeyOf<IIssue> | null;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null;
userAuth: UserAuth;
@ -42,17 +36,12 @@ type Props = {
export const SingleBoard: React.FC<Props> = ({
type,
currentState,
bgColor,
groupTitle,
groupedByIssues,
selectedGroup,
members,
handleEditIssue,
makeIssueCopy,
addIssueToState,
handleDeleteIssue,
openIssuesListModal,
orderBy,
handleTrashBox,
removeIssue,
userAuth,
@ -60,35 +49,24 @@ export const SingleBoard: React.FC<Props> = ({
// collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true);
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssuesView();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
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;
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"}`}>
<BoardHeader
addIssueToState={addIssueToState}
currentState={currentState}
bgColor={bgColor}
selectedGroup={selectedGroup}
groupTitle={groupTitle}
groupedByIssues={groupedByIssues}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
members={members}
/>
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
@ -115,14 +93,12 @@ export const SingleBoard: React.FC<Props> = ({
</div>
</>
)}
{groupedByIssues[groupTitle].map((issue, index: number) => (
{groupedByIssues?.[groupTitle].map((issue, index) => (
<Draggable
key={issue.id}
draggableId={issue.id}
index={index}
isDragDisabled={
isNotAllowed || selectedGroup === "created_by" || selectedGroup === "assignees"
}
isDragDisabled={isNotAllowed}
>
{(provided, snapshot) => (
<SingleBoardIssue
@ -130,16 +106,17 @@ export const SingleBoard: React.FC<Props> = ({
provided={provided}
snapshot={snapshot}
type={type}
issue={issue}
index={index}
selectedGroup={selectedGroup}
issue={issue}
groupTitle={groupTitle}
properties={properties}
editIssue={() => handleEditIssue(issue)}
makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue}
orderBy={orderBy}
handleTrashBox={handleTrashBox}
removeIssue={() => {
removeIssue && removeIssue(issue.bridge);
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id);
}}
userAuth={userAuth}
/>

View File

@ -15,6 +15,7 @@ import {
// services
import issuesService from "services/issues.service";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useToast from "hooks/use-toast";
// components
import {
@ -33,31 +34,30 @@ import {
TrashIcon,
} from "@heroicons/react/24/outline";
// helpers
import { handleIssuesMutation } from "constants/issue";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// types
import {
CycleIssueResponse,
IIssue,
ModuleIssueResponse,
NestedKeyOf,
Properties,
UserAuth,
} from "types";
import { IIssue, Properties, UserAuth } from "types";
// 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?: string;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
issue: IIssue;
selectedGroup: NestedKeyOf<IIssue> | null;
properties: Properties;
groupTitle?: string;
index: number;
selectedGroup: "priority" | "state" | "labels" | null;
editIssue: () => void;
makeIssueCopy: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
orderBy: NestedKeyOf<IIssue> | null;
handleTrashBox: (isDragging: boolean) => void;
userAuth: UserAuth;
};
@ -67,13 +67,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
provided,
snapshot,
issue,
selectedGroup,
properties,
index,
selectedGroup,
editIssue,
makeIssueCopy,
removeIssue,
groupTitle,
handleDeleteIssue,
orderBy,
handleTrashBox,
userAuth,
}) => {
@ -81,6 +82,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const { orderBy } = useIssuesView();
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -91,75 +94,55 @@ export const SingleBoardIssue: React.FC<Props> = ({
if (!workspaceSlug || !projectId) return;
if (cycleId)
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(cycleId as string),
(prevData) => {
const updatedIssues = (prevData ?? []).map((p) => {
if (p.issue_detail.id === issue.id) {
return {
...p,
issue_detail: {
...p.issue_detail,
...formData,
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
},
};
}
return p;
});
return [...updatedIssues];
},
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
CYCLE_ISSUES_WITH_PARAMS(cycleId as string),
(prevData) =>
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
false
);
if (moduleId)
mutate<ModuleIssueResponse[]>(
MODULE_ISSUES(moduleId as string),
(prevData) => {
const updatedIssues = (prevData ?? []).map((p) => {
if (p.issue_detail.id === issue.id) {
return {
...p,
issue_detail: {
...p.issue_detail,
...formData,
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
},
};
}
return p;
});
return [...updatedIssues];
},
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
MODULE_ISSUES_WITH_PARAMS(moduleId as string),
(prevData) =>
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
false
);
mutate<IIssue[]>(
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issue.id)
return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list };
return p;
}),
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
false
);
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
.then((res) => {
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string));
if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
})
.catch((error) => {
console.log(error);
});
},
[workspaceSlug, projectId, cycleId, moduleId, issue]
[workspaceSlug, projectId, cycleId, moduleId, issue, groupTitle, index, selectedGroup]
);
const getStyle = (
@ -168,9 +151,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
) => {
if (orderBy === "sort_order") return style;
if (!snapshot.isDragging) return {};
if (!snapshot.isDropAnimating) {
return style;
}
if (!snapshot.isDropAnimating) return style;
return {
...style,
@ -301,7 +282,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
{properties.labels && issue.label_details.length > 0 && (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
<div
key={label.id}
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}
</span>
</div>
))}
</div>
)}

View File

@ -10,7 +10,7 @@ import issuesService from "services/issues.service";
import stateService from "services/state.service";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useIssueView from "hooks/use-issue-view";
import useIssuesView from "hooks/use-issues-view";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// 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 { PRIORITIES } from "constants/project";
type Props = {
issues?: IIssue[];
};
export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
export const IssuesFilterView: React.FC = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -44,12 +40,12 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
groupByProperty,
setGroupByProperty,
setOrderBy,
setFilterIssue,
orderBy,
filterIssue,
filters,
setFilters,
resetFilterToDefault,
setNewFilterDefaultView,
} = useIssueView(issues ?? []);
} = useIssuesView();
const [properties, setProperties] = useIssuesProperties(
workspaceSlug as string,
@ -79,208 +75,182 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
);
return (
<>
{issues && issues.length > 0 && (
<div className="flex items-center gap-2">
<div className="flex items-center gap-x-1">
<button
type="button"
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()}
>
<ListBulletIcon className="h-4 w-4" />
</button>
<button
type="button"
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()}
>
<Squares2X2Icon className="h-4 w-4" />
</button>
</div>
<CustomMenu
label={
<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">
Filters
</span>
}
<div className="flex items-center gap-2">
<div className="flex items-center gap-x-1">
<button
type="button"
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()}
>
<ListBulletIcon className="h-4 w-4" />
</button>
<button
type="button"
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()}
>
<Squares2X2Icon className="h-4 w-4" />
</button>
</div>
<CustomMenu
customButton={
<button
type="button"
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"
>
<h4 className="px-1 py-2 font-medium">Status</h4>
{statesList?.map((state) => (
<CustomMenu.MenuItem onClick={() => {}}>
<>{state.name}</>
</CustomMenu.MenuItem>
))}
<h4 className="px-1 py-2 font-medium">Members</h4>
{members?.map((member) => (
<CustomMenu.MenuItem onClick={() => {}}>
<>
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name + " " + member.member.last_name
: member.member.email}
</>
</CustomMenu.MenuItem>
))}
<h4 className="px-1 py-2 font-medium">Labels</h4>
{issueLabels?.map((label) => (
<CustomMenu.MenuItem onClick={() => {}}>
<>{label.name}</>
</CustomMenu.MenuItem>
))}
<h4 className="px-1 py-2 font-medium">Priority</h4>
{PRIORITIES?.map((priority) => (
<CustomMenu.MenuItem onClick={() => {}}>
<span className="capitalize">{priority ?? "None"}</span>
</CustomMenu.MenuItem>
))}
</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>
Filters
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
</button>
}
optionsPosition="right"
>
<CustomMenu.MenuItem
onClick={() =>
setFilters({
assignees: ["72d6ad43-41ff-4907-9980-2f5ee8745ad3"],
})
}
>
Member- Aaryan
</CustomMenu.MenuItem>
</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"
}`}
>
View
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute 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">
{issues && (
<div className="space-y-4 pb-3">
<div className="flex items-center justify-between">
<h4 className="text-sm text-gray-600">Group by</h4>
<CustomMenu
label={
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
width="lg"
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute 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="space-y-4 pb-3">
<div className="flex items-center justify-between">
<h4 className="text-sm text-gray-600">Group by</h4>
<CustomMenu
label={
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
"Select"
}
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) =>
issueView === "kanban" && option.key === null ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupByProperty(option.key)}
>
{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>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</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 === 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 modulesService from "services/modules.service";
// hooks
import useIssueView from "hooks/use-issue-view";
import useIssuesView from "hooks/use-issues-view";
// components
import { AllLists, AllBoards } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// icons
import { TrashIcon } from "@heroicons/react/24/outline";
import { PlusIcon, RectangleStackIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
@ -26,32 +26,29 @@ import { CycleIssueResponse, IIssue, ModuleIssueResponse, UserAuth } from "types
// fetch-keys
import {
CYCLE_ISSUES,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES,
PROJECT_ISSUES_LIST,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
PROJECT_MEMBERS,
STATE_LIST,
} from "constants/fetch-keys";
import { EmptySpace, EmptySpaceItem } from "components/ui";
type Props = {
type?: "issue" | "cycle" | "module";
issues: IIssue[];
openIssuesListModal?: () => void;
userAuth: UserAuth;
};
export const IssuesView: React.FC<Props> = ({
type = "issue",
issues,
openIssuesListModal,
userAuth,
}) => {
export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModal, userAuth }) => {
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
>(undefined);
// updates issue modal
// update issue modal
const [editIssueModal, setEditIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<
(IIssue & { actionType: "edit" | "delete" }) | undefined
@ -68,11 +65,13 @@ export const IssuesView: React.FC<Props> = ({
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const {
issueView,
groupedByIssues,
issueView,
groupByProperty: selectedGroup,
orderBy,
} = useIssueView(issues);
filters,
setFilters,
} = useIssuesView();
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
@ -101,7 +100,7 @@ export const IssuesView: React.FC<Props> = ({
(result: DropResult) => {
setTrashBox(false);
if (!result.destination || !workspaceSlug || !projectId) return;
if (!result.destination || !workspaceSlug || !projectId || !groupedByIssues) return;
const { source, destination } = result;
@ -156,90 +155,99 @@ export const IssuesView: React.FC<Props> = ({
draggedItem.sort_order = newSortOrder;
}
if (orderBy === "sort_order" || source.droppableId !== destination.droppableId) {
const sourceGroup = source.droppableId; // source group id
const destinationGroup = destination.droppableId; // destination 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;
else if (selectedGroup === "state_detail.name") {
const destinationState = states?.find((s) => s.name === destinationGroup);
else if (selectedGroup === "state") draggedItem.state = destinationGroup;
}
if (!destinationState) return;
const sourceGroup = source.droppableId; // source group id
draggedItem.state = destinationState.id;
draggedItem.state_detail = destinationState;
}
if (cycleId)
mutate<CycleIssueResponse[]>(
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),
// TODO: move this mutation logic to a separate function
if (cycleId)
mutate<{
[key: string]: IIssue[];
}>(
CYCLE_ISSUES_WITH_PARAMS(cycleId as string),
(prevData) => {
if (!prevData) return prevData;
const updatedIssues = prevData.map((i) => {
if (i.id === draggedItem.id) return draggedItem;
const sourceGroupArray = prevData[sourceGroup];
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
);
// patch request
issuesService
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
priority: draggedItem.priority,
state: draggedItem.state,
sort_order: draggedItem.sort_order,
})
.then((res) => {
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
});
}
// patch request
issuesService
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
priority: draggedItem.priority,
state: draggedItem.state,
sort_order: draggedItem.sort_order,
})
.then(() => {
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
});
}
},
[
@ -250,17 +258,15 @@ export const IssuesView: React.FC<Props> = ({
projectId,
selectedGroup,
orderBy,
states,
handleDeleteIssue,
]
);
const addIssueToState = useCallback(
(groupTitle: string, stateId: string | null) => {
(groupTitle: string) => {
setCreateIssueModal(true);
if (selectedGroup)
setPreloadedData({
state: stateId ?? undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
@ -372,69 +378,116 @@ export const IssuesView: React.FC<Props> = ({
isOpen={deleteIssueModal}
data={issueToDelete}
/>
<div className="relative">
<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 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}
<div className="flex items-center gap-2">
{Object.keys(filters).map((key) => {
if (filters[key as keyof typeof filters] !== null)
return (
<button
key={key}
type="button"
className="rounded bg-black p-2 text-xs text-white"
onClick={() =>
setFilters({
[key]: null,
})
}
>
<TrashIcon className="h-4 w-4" />
Drop issue here to delete
</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>
Remove {key} filter
</button>
);
})}
</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
import useIssueView from "hooks/use-issue-view";
import useIssuesView from "hooks/use-issues-view";
// components
import { SingleList } from "components/core/list-view/single-list";
// types
@ -8,7 +8,6 @@ import { IIssue, IProjectMember, IState, UserAuth } from "types";
// types
type Props = {
type: "issue" | "cycle" | "module";
issues: IIssue[];
states: IState[] | undefined;
members: IProjectMember[] | undefined;
addIssueToState: (groupTitle: string, stateId: string | null) => void;
@ -22,7 +21,6 @@ type Props = {
export const AllLists: React.FC<Props> = ({
type,
issues,
states,
members,
addIssueToState,
@ -33,44 +31,35 @@ export const AllLists: React.FC<Props> = ({
removeIssue,
userAuth,
}) => {
const { groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
return (
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => {
const currentState =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)
: 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";
<>
{groupedByIssues && (
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => {
const stateId = selectedGroup === "state" ? singleGroup : null;
return (
<SingleList
key={singleGroup}
type={type}
currentState={currentState}
bgColor={bgColor}
groupTitle={singleGroup}
groupedByIssues={groupedByIssues}
selectedGroup={selectedGroup}
members={members}
addIssueToState={() => addIssueToState(singleGroup, stateId)}
makeIssueCopy={makeIssueCopy}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
removeIssue={removeIssue}
userAuth={userAuth}
/>
);
})}
</div>
return (
<SingleList
key={singleGroup}
type={type}
groupTitle={singleGroup}
groupedByIssues={groupedByIssues}
selectedGroup={selectedGroup}
members={members}
addIssueToState={() => addIssueToState(singleGroup, stateId)}
makeIssueCopy={makeIssueCopy}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
removeIssue={removeIssue}
userAuth={userAuth}
/>
);
})}
</div>
)}
</>
);
};

View File

@ -16,7 +16,8 @@ import {
ViewPrioritySelect,
ViewStateSelect,
} from "components/issues/view-select";
// hooks
import useIssueView from "hooks/use-issues-view";
// ui
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
// icons
@ -28,16 +29,23 @@ import {
} from "@heroicons/react/24/outline";
// helpers
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { handleIssuesMutation } from "constants/issue";
// types
import { CycleIssueResponse, IIssue, ModuleIssueResponse, Properties, UserAuth } from "types";
import { IIssue, Properties, UserAuth } from "types";
// 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?: string;
issue: IIssue;
properties: Properties;
groupTitle?: string;
editIssue: () => void;
index: number;
makeIssueCopy: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
@ -49,8 +57,10 @@ export const SingleListIssue: React.FC<Props> = ({
issue,
properties,
editIssue,
index,
makeIssueCopy,
removeIssue,
groupTitle,
handleDeleteIssue,
userAuth,
}) => {
@ -63,80 +73,62 @@ export const SingleListIssue: React.FC<Props> = ({
const { setToastAlert } = useToast();
const { groupByProperty: selectedGroup } = useIssueView();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId) return;
if (cycleId)
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(cycleId as string),
(prevData) => {
const updatedIssues = (prevData ?? []).map((p) => {
if (p.issue_detail.id === issue.id) {
return {
...p,
issue_detail: {
...p.issue_detail,
...formData,
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
},
};
}
return p;
});
return [...updatedIssues];
},
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
CYCLE_ISSUES_WITH_PARAMS(cycleId as string),
(prevData) =>
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
false
);
if (moduleId)
mutate<ModuleIssueResponse[]>(
MODULE_ISSUES(moduleId as string),
(prevData) => {
const updatedIssues = (prevData ?? []).map((p) => {
if (p.issue_detail.id === issue.id) {
return {
...p,
issue_detail: {
...p.issue_detail,
...formData,
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
},
};
}
return p;
});
return [...updatedIssues];
},
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
MODULE_ISSUES_WITH_PARAMS(moduleId as string),
(prevData) =>
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
false
);
mutate<IIssue[]>(
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issue.id)
return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list };
return p;
}),
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
false
);
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
.then((res) => {
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
if (moduleId) mutate(MODULE_ISSUES(moduleId as string));
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string));
if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
})
.catch((error) => {
console.log(error);
});
},
[workspaceSlug, projectId, cycleId, moduleId, issue]
[workspaceSlug, projectId, cycleId, moduleId, issue, groupTitle, index, selectedGroup]
);
const handleCopyText = () => {

View File

@ -12,7 +12,7 @@ import { getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
import { IIssue, IProjectMember, IState, UserAuth } from "types";
import { CustomMenu } from "components/ui";
type Props = {
@ -23,7 +23,7 @@ type Props = {
groupedByIssues: {
[key: string]: IIssue[];
};
selectedGroup: NestedKeyOf<IIssue> | null;
selectedGroup: "priority" | "state" | "labels" | null;
members: IProjectMember[] | undefined;
addIssueToState: () => 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 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 (
<Disclosure key={groupTitle} as="div" defaultOpen>
{({ open }) => (
@ -82,7 +66,7 @@ export const SingleList: React.FC<Props> = ({
>
<Disclosure.Button>
<div className="flex items-center gap-x-3">
{selectedGroup !== null && selectedGroup === "state_detail.name" ? (
{selectedGroup !== null && selectedGroup === "state" ? (
<span>
{currentState && getStateGroupIcon(currentState.group, "20", "20", bgColor)}
</span>
@ -91,11 +75,7 @@ export const SingleList: React.FC<Props> = ({
)}
{selectedGroup !== null ? (
<h2 className="text-xl font-semibold capitalize leading-6 text-gray-800">
{selectedGroup === "created_by"
? createdBy
: selectedGroup === "assignees"
? assignees
: addSpaceIfCamelCase(groupTitle)}
{addSpaceIfCamelCase(groupTitle)}
</h2>
) : (
<h2 className="font-medium leading-5">All Issues</h2>
@ -105,7 +85,6 @@ export const SingleList: React.FC<Props> = ({
</span>
</div>
</Disclosure.Button>
{type === "issue" ? (
<button
type="button"
@ -145,17 +124,19 @@ export const SingleList: React.FC<Props> = ({
<Disclosure.Panel>
{groupedByIssues[groupTitle] ? (
groupedByIssues[groupTitle].length > 0 ? (
groupedByIssues[groupTitle].map((issue: IIssue) => (
groupedByIssues[groupTitle].map((issue, index) => (
<SingleListIssue
key={issue.id}
type={type}
issue={issue}
properties={properties}
groupTitle={groupTitle}
index={index}
editIssue={() => handleEditIssue(issue)}
makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue}
removeIssue={() => {
removeIssue && removeIssue(issue.bridge);
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id);
}}
userAuth={userAuth}
/>

View File

@ -179,6 +179,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
{members?.map((member, index) => {
const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id));
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
if (totalArray.length > 0) {
return (
<SingleProgressStats
@ -223,9 +224,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
)}
</Tab.Panel>
<Tab.Panel as="div" className="flex w-full flex-col ">
{issueLabels?.map((issue, index) => {
const totalArray = issues?.filter((i) => i.labels?.includes(issue.id));
{issueLabels?.map((label, index) => {
const totalArray = issues?.filter((i) => i.labels?.includes(label.id));
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
if (totalArray.length > 0) {
return (
<SingleProgressStats
@ -235,10 +237,11 @@ export const SidebarProgressStats: React.FC<Props> = ({
<span
className="block h-3 w-3 rounded-full "
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>
}
completed={completeArray.length}

View File

@ -6,14 +6,23 @@ type TSingleProgressStatsProps = {
title: any;
completed: number;
total: number;
onClick?: () => void;
selected?: boolean;
};
export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
title,
completed,
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-end gap-1 px-2">
<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 Image from "next/image";
import { mutate } from "swr";
import useSWR, { mutate } from "swr";
// 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 { renderDateFormat, renderShortDate } from "helpers/date-time.helper";
// types
import { CycleIssueResponse, ICycle, IIssue } from "types";
import { ICycle, IIssue } from "types";
// fetch-keys
import { CYCLE_DETAILS } from "constants/fetch-keys";
import { CYCLE_DETAILS, CYCLE_ISSUES } from "constants/fetch-keys";
type Props = {
issues: IIssue[];
cycle: ICycle | undefined;
isOpen: boolean;
cycleIssues: CycleIssueResponse[];
cycleStatus: string;
};
export const CycleDetailsSidebar: React.FC<Props> = ({
issues,
cycle,
isOpen,
cycleIssues,
cycleStatus,
}) => {
export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatus }) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [startDateRange, setStartDateRange] = useState<Date | null>(new Date());
const [endDateRange, setEndDateRange] = useState<Date | null>(null);
@ -69,13 +61,25 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
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 = {
backlog: [],
unstarted: [],
started: [],
cancelled: [],
completed: [],
...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"),
...groupBy(issues ?? [], "state_detail.group"),
};
const { reset } = useForm({
@ -131,9 +135,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
const isStartValid = new Date(`${cycle?.start_date}`) <= new Date();
const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`);
const progressPercentage = cycleIssues
? Math.round((groupedIssues.completed.length / cycleIssues?.length) * 100)
const progressPercentage = issues
? Math.round((groupedIssues.completed.length / issues?.length) * 100)
: null;
return (
<>
<DeleteCycleModal isOpen={cycleDeleteModal} setIsOpen={setCycleDeleteModal} data={cycle} />
@ -305,10 +310,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
<span className="h-4 w-4">
<ProgressBar
value={groupedIssues.completed.length}
maxValue={cycleIssues?.length}
maxValue={issues?.length}
/>
</span>
{groupedIssues.completed.length}/{cycleIssues?.length}
{groupedIssues.completed.length}/{issues?.length}
</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 items-center justify-start gap-2 text-sm">
<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]">
{progressPercentage ? `${progressPercentage}%` : ""}
</span>
@ -359,7 +364,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</span>
<span>
Pending Issues -{" "}
{cycleIssues?.length - groupedIssues.completed.length}{" "}
{issues?.length ?? 0 - groupedIssues.completed.length}{" "}
</span>
</div>
@ -376,7 +381,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div>
<div className="relative h-40 w-80">
<ProgressChart
issues={issues}
issues={issues ?? []}
start={cycle?.start_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>
</div>
{issues.length > 0 ? (
{(issues?.length ?? 0) > 0 ? (
<Disclosure.Button>
<ChevronDownIcon
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
@ -419,9 +424,12 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div>
<Transition show={open}>
<Disclosure.Panel>
{issues.length > 0 ? (
{(issues?.length ?? 0) > 0 ? (
<div className=" h-full w-full py-4">
<SidebarProgressStats issues={issues} groupedIssues={groupedIssues} />
<SidebarProgressStats
issues={issues ?? []}
groupedIssues={groupedIssues}
/>
</div>
) : (
""

View File

@ -30,7 +30,6 @@ import { capitalizeFirstLetter, copyTextToClipboard, truncateText } from "helper
import {
CompletedCyclesResponse,
CurrentAndUpcomingCyclesResponse,
CycleIssueResponse,
DraftCyclesResponse,
ICycle,
} from "types";
@ -65,7 +64,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
const { workspaceSlug, projectId } = router.query;
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
? () => 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) =>
partialUpdateIssue({
target_date: val,
priority: issue.priority,
state: issue.state,
})
}
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}

View File

@ -25,8 +25,10 @@ export const ViewPrioritySelect: React.FC<Props> = ({
isNotAllowed,
}) => (
<CustomSelect
value={issue.state}
onChange={(data: string) => partialUpdateIssue({ priority: data })}
value={issue.priority}
onChange={(data: string) =>
partialUpdateIssue({ priority: data, state: issue.state, target_date: issue.target_date })
}
maxHeight="md"
customButton={
<button

View File

@ -58,7 +58,13 @@ export const ViewStateSelect: React.FC<Props> = ({
return (
<CustomSearchSelect
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}
label={
<Tooltip

View File

@ -33,11 +33,11 @@ import ProgressChart from "components/core/sidebar/progress-chart";
// ui
import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui";
// 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 { groupBy } from "helpers/array.helper";
// types
import { IIssue, IModule, ModuleIssueResponse, ModuleLink, UserAuth } from "types";
import { IIssue, IModule, ModuleLink, UserAuth } from "types";
// fetch-keys
import { MODULE_DETAILS } from "constants/fetch-keys";
// constant
@ -55,7 +55,7 @@ type Props = {
issues: IIssue[];
module?: IModule;
isOpen: boolean;
moduleIssues: ModuleIssueResponse[] | undefined;
moduleIssues?: IIssue[];
userAuth: UserAuth;
};

View File

@ -21,7 +21,6 @@ import { groupBy, orderArrayBy } from "helpers/array.helper";
import { orderStateGroups } from "helpers/state.helper";
// types
import { IState } from "types";
import { StateGroup } from "components/states";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
import { getStateGroupIcon } from "components/icons";

View File

@ -1,3 +1,5 @@
import { IIssueFilterOptions } from "types";
export const CURRENT_USER = "CURRENT_USER";
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
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) =>
`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_PROPERTIES = (projectId: string) =>
`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_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_CURRENT_AND_UPCOMING_LIST = (projectId: string) =>
`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_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 VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId}`;

View File

@ -1,15 +1,17 @@
// types
import { IIssue, NestedKeyOf } from "types";
export const GROUP_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
{ name: "State", key: "state_detail.name" },
export const GROUP_BY_OPTIONS: Array<{
name: string;
key: "state" | "priority" | "labels" | null;
}> = [
{ name: "State", key: "state" },
{ name: "Priority", key: "priority" },
{ name: "Created By", key: "created_by" },
{ name: "Assignee", key: "assignees" },
{ name: "Labels", key: "labels" },
{ 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: "Last created", key: "created_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<{
name: string;
key: "activeIssue" | "backlogIssue" | null;
key: "active" | "backlog" | null;
}> = [
{
name: "All",
@ -26,10 +28,78 @@ export const FILTER_ISSUE_OPTIONS: Array<{
},
{
name: "Active Issues",
key: "activeIssue",
key: "active",
},
{
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 useSWR from "swr";
import useSWR, { mutate } from "swr";
// components
import ToastAlert from "components/toast-alert";
// services
import projectService from "services/project.service";
// types
import type { IIssue, NestedKeyOf } from "types";
import { IIssueFilterOptions, IProjectMember, NestedKeyOf } from "types";
// 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);
type IssueViewProps = {
issueView: "list" | "kanban" | null;
groupByProperty: NestedKeyOf<IIssue> | null;
filterIssue: "activeIssue" | "backlogIssue" | null;
orderBy: NestedKeyOf<IIssue> | null;
issueView: "list" | "kanban";
groupByProperty: "state" | "priority" | "labels" | null;
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
filters: IIssueFilterOptions;
};
type ReducerActionType = {
@ -27,20 +32,16 @@ type ReducerActionType = {
| "REHYDRATE_THEME"
| "SET_ISSUE_VIEW"
| "SET_ORDER_BY_PROPERTY"
| "SET_FILTER_ISSUES"
| "SET_FILTERS"
| "SET_GROUP_BY_PROPERTY"
| "RESET_TO_DEFAULT";
payload?: Partial<IssueViewProps>;
};
type ContextType = {
orderBy: NestedKeyOf<IIssue> | null;
issueView: "list" | "kanban" | null;
groupByProperty: NestedKeyOf<IIssue> | null;
filterIssue: "activeIssue" | "backlogIssue" | null;
setGroupByProperty: (property: NestedKeyOf<IIssue> | null) => void;
setOrderBy: (property: NestedKeyOf<IIssue> | null) => void;
setFilterIssue: (property: "activeIssue" | "backlogIssue" | null) => void;
type ContextType = IssueViewProps & {
setGroupByProperty: (property: "state" | "priority" | "labels" | null) => void;
setOrderBy: (property: "created_at" | "updated_at" | "priority" | "sort_order") => void;
setFilters: (filters: Partial<IIssueFilterOptions>) => void;
resetFilterToDefault: () => void;
setNewFilterDefaultView: () => void;
setIssueViewToKanban: () => void;
@ -48,10 +49,10 @@ type ContextType = {
};
type StateType = {
issueView: "list" | "kanban" | null;
groupByProperty: NestedKeyOf<IIssue> | null;
filterIssue: "activeIssue" | "backlogIssue" | null;
orderBy: NestedKeyOf<IIssue> | null;
issueView: "list" | "kanban";
groupByProperty: "state" | "priority" | "labels" | null;
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
filters: IIssueFilterOptions;
};
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
@ -59,7 +60,13 @@ export const initialState: StateType = {
issueView: "list",
groupByProperty: null,
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) => {
@ -69,6 +76,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
case "REHYDRATE_THEME": {
let collapsed: any = localStorage.getItem("collapsed");
collapsed = collapsed ? JSON.parse(collapsed) : false;
return { ...initialState, ...payload, collapsed };
}
@ -77,6 +85,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
...state,
issueView: payload?.issueView || "list",
};
return {
...state,
...newState,
@ -88,6 +97,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
...state,
groupByProperty: payload?.groupByProperty || null,
};
return {
...state,
...newState,
@ -97,19 +107,24 @@ export const reducer: ReducerFunctionType = (state, action) => {
case "SET_ORDER_BY_PROPERTY": {
const newState = {
...state,
orderBy: payload?.orderBy || null,
orderBy: payload?.orderBy || "created_at",
};
return {
...state,
...newState,
};
}
case "SET_FILTER_ISSUES": {
case "SET_FILTERS": {
const newState = {
...state,
filterIssue: payload?.filterIssue || null,
filters: {
...state.filters,
...payload,
},
};
return {
...state,
...newState,
@ -135,8 +150,21 @@ const saveDataToServer = async (workspaceSlug: string, projectID: string, state:
});
};
const setNewDefault = async (workspaceSlug: string, projectID: string, state: any) => {
await projectService.setProjectView(workspaceSlug, projectID, {
const setNewDefault = async (workspaceSlug: string, projectId: string, state: any) => {
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,
default_props: state,
});
@ -146,7 +174,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
const [state, dispatch] = useReducer(reducer, initialState);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
@ -162,10 +190,11 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
issueView: "kanban",
},
});
dispatch({
type: "SET_GROUP_BY_PROPERTY",
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, {
...state,
issueView: "kanban",
groupByProperty: "state_detail.name",
groupByProperty: "state",
});
}, [workspaceSlug, projectId, state]);
@ -185,6 +214,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
issueView: "list",
},
});
dispatch({
type: "SET_GROUP_BY_PROPERTY",
payload: {
@ -194,15 +224,28 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
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, {
...state,
issueView: "list",
groupByProperty: null,
});
}, [workspaceSlug, projectId, state]);
}, [workspaceSlug, projectId, state, mutateMyViewProps]);
const setGroupByProperty = useCallback(
(property: NestedKeyOf<IIssue> | null) => {
(property: "state" | "priority" | "labels" | null) => {
dispatch({
type: "SET_GROUP_BY_PROPERTY",
payload: {
@ -211,16 +254,29 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
});
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, {
...state,
groupByProperty: property,
});
},
[projectId, workspaceSlug, state]
[projectId, workspaceSlug, state, mutateMyViewProps]
);
const setOrderBy = useCallback(
(property: NestedKeyOf<IIssue> | null) => {
(property: "created_at" | "updated_at" | "priority" | "sort_order") => {
dispatch({
type: "SET_ORDER_BY_PROPERTY",
payload: {
@ -229,34 +285,70 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
});
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, {
...state,
orderBy: property,
});
},
[projectId, workspaceSlug, state]
[projectId, workspaceSlug, state, mutateMyViewProps]
);
const setFilterIssue = useCallback(
(property: "activeIssue" | "backlogIssue" | null) => {
const setFilters = useCallback(
(property: Partial<IIssueFilterOptions>) => {
dispatch({
type: "SET_FILTER_ISSUES",
type: "SET_FILTERS",
payload: {
filterIssue: property,
filters: {
...state.filters,
...property,
},
},
});
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, {
...state,
filterIssue: property,
filters: {
...state.filters,
...property,
},
});
},
[projectId, workspaceSlug, state]
[projectId, workspaceSlug, state, mutateMyViewProps]
);
const setNewDefaultView = useCallback(() => {
if (!workspaceSlug || !projectId) return;
setNewDefault(workspaceSlug as string, projectId as string, state).then(() => {
mutateMyViewProps();
});
@ -267,7 +359,9 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
type: "RESET_TO_DEFAULT",
payload: myViewProps?.default_props,
});
if (!workspaceSlug || !projectId) return;
saveDataToServer(workspaceSlug as string, projectId as string, myViewProps?.default_props);
}, [projectId, workspaceSlug, myViewProps]);
@ -278,6 +372,20 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
});
}, [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 (
<issueViewContext.Provider
value={{
@ -286,8 +394,8 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
setGroupByProperty,
orderBy: state.orderBy,
setOrderBy,
filterIssue: state.filterIssue,
setFilterIssue,
filters: state.filters,
setFilters,
resetFilterToDefault: resetToDefault,
setNewFilterDefaultView: setNewDefaultView,
setIssueViewToKanban,

View File

@ -81,7 +81,7 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
useEffect(() => {
dispatch({
type: "REHYDRATE_THEME",
payload: myViewProps?.view_props,
payload: myViewProps?.view_props as any,
});
}, [myViewProps]);

View File

@ -1,5 +1,6 @@
export const debounce = (func: any, wait: number, immediate: boolean = false) => {
let timeout: any;
return function executedFunction(...args: any) {
const later = () => {
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 { GetServerSidePropsContext } from "next";
// icons
import { ArrowLeftIcon, ListBulletIcon, PlusIcon } from "@heroicons/react/24/outline";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { CyclesIcon } from "components/icons";
// lib
import { requiredAdmin, requiredAuth } from "lib/auth";
@ -21,21 +21,15 @@ import issuesServices from "services/issues.service";
import cycleServices from "services/cycles.service";
import projectService from "services/project.service";
// ui
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
import { CustomMenu } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { truncateText } from "helpers/string.helper";
import { getDateRangeStatus } from "helpers/date-time.helper";
// types
import { CycleIssueResponse, UserAuth } from "types";
import { UserAuth } from "types";
// fetch-keys
import {
CYCLE_ISSUES,
CYCLE_LIST,
PROJECT_ISSUES_LIST,
PROJECT_DETAILS,
CYCLE_DETAILS,
} from "constants/fetch-keys";
import { CYCLE_ISSUES, CYCLE_LIST, PROJECT_DETAILS, CYCLE_DETAILS } from "constants/fetch-keys";
const SingleCycle: React.FC<UserAuth> = (props) => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
@ -51,15 +45,6 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
: 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(
workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null,
workspaceSlug && projectId
@ -84,7 +69,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
? 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
? () =>
@ -96,13 +81,6 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
: 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 = () => {
setCycleIssuesListModal(true);
};
@ -164,7 +142,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
<div
className={`flex items-center gap-2 ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}
>
<IssuesFilterView issues={cycleIssuesArray ?? []} />
<IssuesFilterView />
<button
type="button"
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>
}
>
{cycleIssuesArray ? (
cycleIssuesArray.length > 0 ? (
<div className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}>
<IssuesView
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 ?? []}
/>
<div className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}>
<IssuesView type="cycle" userAuth={props} openIssuesListModal={openIssuesListModal} />
</div>
<CycleDetailsSidebar cycleStatus={cycleStatus} cycle={cycleDetails} isOpen={cycleSidebar} />
</AppLayout>
</IssueViewContextProvider>
);

View File

@ -5,7 +5,6 @@ import useSWR from "swr";
// lib
import { requiredAdmin, requiredAuth } from "lib/auth";
// services
import issuesServices from "services/issues.service";
import projectService from "services/project.service";
// layouts
import AppLayout from "layouts/app-layout";
@ -14,31 +13,20 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { IssuesFilterView, IssuesView } from "components/core";
// ui
import { Spinner, EmptySpace, EmptySpaceItem, HeaderButton, EmptyState } from "components/ui";
import { HeaderButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { RectangleStackIcon, PlusIcon } from "@heroicons/react/24/outline";
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import type { UserAuth } from "types";
import type { GetServerSidePropsContext, NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// image
import emptyIssue from "public/empty-state/empty-issue.svg";
import { PROJECT_DETAILS } from "constants/fetch-keys";
const ProjectIssues: NextPage<UserAuth> = (props) => {
const router = useRouter();
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(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
@ -57,7 +45,7 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
}
right={
<div className="flex items-center gap-2">
<IssuesFilterView issues={projectIssues?.filter((p) => p.parent === null) ?? []} />
<IssuesFilterView />
<HeaderButton
Icon={PlusIcon}
label="Add Issue"
@ -71,26 +59,7 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
</div>
}
>
{projectIssues ? (
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>
)}
<IssuesView userAuth={props} />
</AppLayout>
</IssueViewContextProvider>
);

View File

@ -30,7 +30,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { IModule, ModuleIssueResponse, UserAuth } from "types";
import { IModule, UserAuth } from "types";
// fetch-keys
import {
@ -63,7 +63,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
: null
);
const { data: moduleIssues } = useSWR<ModuleIssueResponse[]>(
const { data: moduleIssues } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_ISSUES(moduleId as string) : null,
workspaceSlug && projectId && moduleId
? () =>
@ -87,13 +87,6 @@ const SingleModule: React.FC<UserAuth> = (props) => {
: 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[] }) => {
if (!workspaceSlug || !projectId) return;
@ -153,7 +146,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
<div
className={`flex items-center gap-2 ${moduleSidebar ? "mr-[24rem]" : ""} duration-300`}
>
<IssuesFilterView issues={moduleIssuesArray ?? []} />
<IssuesFilterView />
<button
type="button"
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>
}
>
{moduleIssuesArray ? (
moduleIssuesArray.length > 0 ? (
{moduleIssues ? (
moduleIssues.length > 0 ? (
<div className={`h-full ${moduleSidebar ? "mr-[24rem]" : ""} duration-300`}>
<IssuesView
type="module"
issues={moduleIssuesArray ?? []}
userAuth={props}
openIssuesListModal={openIssuesListModal}
/>
@ -213,7 +205,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
</div>
)}
<ModuleDetailsSidebar
issues={moduleIssuesArray ?? []}
issues={moduleIssues ?? []}
module={moduleDetails}
isOpen={moduleSidebar}
moduleIssues={moduleIssues}

View File

@ -1,7 +1,15 @@
// services
import APIService from "services/api.service";
// 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;
@ -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(
`/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(
workspaceSlug: string,
projectId: string,
@ -88,18 +116,28 @@ class ProjectCycleServices extends APIService {
});
}
async cycleDateCheck(workspaceSlug: string, projectId: string, data: {
start_date: string,
end_date: string
}): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data)
async cycleDateCheck(
workspaceSlug: string,
projectId: string,
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)
.catch((error) => {
throw error?.response?.data;
});
}
async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise<CurrentAndUpcomingCyclesResponse> {
async getCurrentAndUpcomingCycles(
workspaceSlug: string,
projectId: string
): Promise<CurrentAndUpcomingCyclesResponse> {
return this.get(
`/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> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`
)
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getCompletedCycles(workspaceSlug: string, projectId: string): Promise<CompletedCyclesResponse> {
async getCompletedCycles(
workspaceSlug: string,
projectId: string
): Promise<CompletedCyclesResponse> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/`
)
@ -136,21 +175,29 @@ class ProjectCycleServices extends APIService {
cycle: string;
}
): 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)
.catch((error) => {
throw error?.response?.data;
});
}
async removeCycleFromFavorites(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/${cycleId}/`)
async removeCycleFromFavorites(
workspaceSlug: string,
projectId: string,
cycleId: string
): Promise<any> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/${cycleId}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
export default new ProjectCycleServices();

View File

@ -1,7 +1,7 @@
// services
import APIService from "services/api.service";
// type
import type { IIssue, IIssueActivity, IIssueComment } from "types";
import type { IIssue, IIssueActivity, IIssueComment, IIssueViewOptions } from "types";
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> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`)
.then((response) => response?.data)

View File

@ -1,7 +1,7 @@
// services
import APIService from "services/api.service";
// types
import type { IModule } from "types";
import type { IIssueViewOptions, IModule, ModuleIssueResponse, IIssue } from "types";
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(
`/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(
workspaceSlug: string,
projectId: string,
@ -159,15 +184,24 @@ class ProjectIssuesServices extends APIService {
module: string;
}
): 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)
.catch((error) => {
throw error?.response?.data;
});
}
async removeModuleFromFavorites(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/${moduleId}/`)
async removeModuleFromFavorites(
workspaceSlug: string,
projectId: string,
moduleId: string
): Promise<any> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/${moduleId}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;

View File

@ -67,7 +67,7 @@ export interface IIssue {
blockers: any[];
blockers_list: string[];
blocks_list: string[];
bridge: string;
bridge_id?: string | null;
completed_at: Date;
created_at: Date;
created_by: string;
@ -206,3 +206,17 @@ export interface IIssueActivity {
issue_comment: string | null;
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 {
cover_image: string | null;
@ -34,11 +34,10 @@ export interface IFavoriteProject {
}
type ProjectViewTheme = {
collapsed: boolean;
issueView: "list" | "kanban" | null;
groupByProperty: NestedKeyOf<IIssue> | null;
filterIssue: "activeIssue" | "backlogIssue" | null;
orderBy: NestedKeyOf<IIssue> | null;
issueView: "list" | "kanban";
groupByProperty: "state" | "priority" | "labels" | null;
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
filters: IIssueFilterOptions;
};
export interface IProjectMember {