forked from github/plane
fix: mutation for issue update on both kanban & list (#436)
* refactor: issues filter logic * fix: removed fetch logic from hooks * feat: filter by assignee and label * chore: remove filter buttons * feat: filter options * fix: mutation for issue update on both kanban & list --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
636e8e6c60
commit
928ebdf632
@ -1,16 +1,14 @@
|
||||
// hooks
|
||||
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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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 = () => {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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 ">
|
||||
|
@ -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>
|
||||
) : (
|
||||
""
|
||||
|
@ -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)
|
||||
|
@ -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"}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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}`;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -1,126 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// contexts
|
||||
import { issueViewContext } from "contexts/issue-view.context";
|
||||
// helpers
|
||||
import { groupBy, orderArrayBy } from "helpers/array.helper";
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { IIssue, IState } from "types";
|
||||
// fetch-keys
|
||||
import { STATE_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
const useIssueView = (projectIssues: IIssue[]) => {
|
||||
const {
|
||||
issueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
filterIssue,
|
||||
setFilterIssue,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
setIssueViewToKanban,
|
||||
setIssueViewToList,
|
||||
} = useContext(issueViewContext);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const states = getStatesList(stateGroups ?? {});
|
||||
|
||||
let groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
} = {};
|
||||
|
||||
const groupIssues = (states: IState[], issues: IIssue[]) => ({
|
||||
...(groupByProperty === "state_detail.name"
|
||||
? Object.fromEntries(
|
||||
states
|
||||
?.sort((a, b) => a.sequence - b.sequence)
|
||||
?.map((state) => [
|
||||
state.name,
|
||||
issues.filter((issue) => issue.state === state.name) ?? [],
|
||||
]) ?? []
|
||||
)
|
||||
: groupByProperty === "priority"
|
||||
? Object.fromEntries(
|
||||
PRIORITIES.map((priority) => [
|
||||
priority,
|
||||
issues.filter((issue) => issue.priority === priority) ?? [],
|
||||
])
|
||||
)
|
||||
: {}),
|
||||
...groupBy(issues ?? [], groupByProperty ?? ""),
|
||||
});
|
||||
|
||||
groupedByIssues = groupIssues(states ?? [], projectIssues);
|
||||
|
||||
if (filterIssue) {
|
||||
if (filterIssue === "activeIssue") {
|
||||
const filteredStates = states?.filter(
|
||||
(s) => s.group === "started" || s.group === "unstarted"
|
||||
);
|
||||
const filteredIssues = projectIssues.filter(
|
||||
(i) => i.state_detail.group === "started" || i.state_detail.group === "unstarted"
|
||||
);
|
||||
|
||||
groupedByIssues = groupIssues(filteredStates ?? [], filteredIssues);
|
||||
} else if (filterIssue === "backlogIssue") {
|
||||
const filteredStates = states?.filter(
|
||||
(s) => s.group === "backlog" || s.group === "cancelled"
|
||||
);
|
||||
const filteredIssues = projectIssues.filter(
|
||||
(i) => i.state_detail.group === "backlog" || i.state_detail.group === "cancelled"
|
||||
);
|
||||
|
||||
groupedByIssues = groupIssues(filteredStates ?? [], filteredIssues);
|
||||
}
|
||||
}
|
||||
|
||||
if (orderBy) {
|
||||
groupedByIssues = Object.fromEntries(
|
||||
Object.entries(groupedByIssues).map(([key, value]) => [
|
||||
key,
|
||||
orderArrayBy(value, orderBy, orderBy === "sort_order" ? "ascending" : "descending"),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (groupByProperty === "priority") {
|
||||
delete groupedByIssues.None;
|
||||
if (orderBy === "priority") setOrderBy("created_at");
|
||||
}
|
||||
|
||||
return {
|
||||
groupedByIssues,
|
||||
issueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
filterIssue,
|
||||
setFilterIssue,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
setIssueViewToKanban,
|
||||
setIssueViewToList,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export default useIssueView;
|
121
apps/app/hooks/use-issues-view.tsx
Normal file
121
apps/app/hooks/use-issues-view.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { useContext, useMemo } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// contexts
|
||||
import { issueViewContext } from "contexts/issue-view.context";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
import modulesService from "services/modules.service";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
|
||||
const useIssuesView = () => {
|
||||
const {
|
||||
issueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
filters,
|
||||
setFilters,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
setIssueViewToKanban,
|
||||
setIssueViewToList,
|
||||
} = useContext(issueViewContext);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const params: any = {
|
||||
order_by: orderBy,
|
||||
group_by: groupByProperty,
|
||||
assignees: filters.assignees ? filters.assignees.join(",") : undefined,
|
||||
type: filters.type ? filters.type : undefined,
|
||||
labels: filters.labels ? filters.labels.join(",") : undefined,
|
||||
issue__assignees__id: filters.issue__assignees__id
|
||||
? filters.issue__assignees__id.join(",")
|
||||
: undefined,
|
||||
issue__labels__id: filters.issue__labels__id ? filters.issue__labels__id.join(",") : undefined,
|
||||
};
|
||||
|
||||
const { data: projectIssues } = useSWR(
|
||||
workspaceSlug && projectId && params
|
||||
? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId && params
|
||||
? () =>
|
||||
issuesService.getIssuesWithParams(workspaceSlug as string, projectId as string, params)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cycleIssues } = useSWR(
|
||||
workspaceSlug && projectId && cycleId && params
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId && cycleId && params
|
||||
? () =>
|
||||
cyclesService.getCycleIssuesWithParams(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycleId as string,
|
||||
params
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleIssues } = useSWR(
|
||||
workspaceSlug && projectId && moduleId && params
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId && moduleId && params
|
||||
? () =>
|
||||
modulesService.getModuleIssuesWithParams(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
moduleId as string,
|
||||
params
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const groupedByIssues:
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
| undefined = useMemo(() => {
|
||||
const issuesToGroup = cycleIssues ?? moduleIssues ?? projectIssues;
|
||||
|
||||
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
||||
else return issuesToGroup;
|
||||
}, [projectIssues, cycleIssues, moduleIssues]);
|
||||
|
||||
return {
|
||||
groupedByIssues,
|
||||
issueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
filters,
|
||||
setFilters,
|
||||
params,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
setIssueViewToKanban,
|
||||
setIssueViewToList,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export default useIssuesView;
|
@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { 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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
16
apps/app/types/issues.d.ts
vendored
16
apps/app/types/issues.d.ts
vendored
@ -67,7 +67,7 @@ export interface IIssue {
|
||||
blockers: any[];
|
||||
blockers_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;
|
||||
}
|
||||
|
11
apps/app/types/projects.d.ts
vendored
11
apps/app/types/projects.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import type { IUserLite, IWorkspace } from "./";
|
||||
import type { IIssueFilterOptions, IUserLite, IWorkspace } from "./";
|
||||
|
||||
export interface IProject {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user