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
33 changed files with 1149 additions and 1036 deletions
|
|
@ -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 ">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue