feat: manual ordering of issues (#305)

This commit is contained in:
Aaryan Khandelwal 2023-02-20 19:19:46 +05:30 committed by GitHub
parent 202096500e
commit 818fe3ecf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 85 additions and 32 deletions

View File

@ -29,7 +29,7 @@ type Props = {
addIssueToState: () => void; addIssueToState: () => void;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
orderBy: NestedKeyOf<IIssue> | "manual" | null; orderBy: NestedKeyOf<IIssue> | null;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null; removeIssue: ((bridgeId: string) => void) | null;
userAuth: UserAuth; userAuth: UserAuth;
@ -92,6 +92,22 @@ export const SingleBoard: React.FC<Props> = ({
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
> >
{orderBy !== "sort_order" && (
<>
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} top-0 left-0 h-full w-full bg-indigo-200 opacity-50 pointer-events-none z-[99999998]`}
/>
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 text-xs whitespace-nowrap bg-white p-2 rounded pointer-events-none z-[99999999]`}
>
This board is order by {orderBy}
</div>
</>
)}
{groupedByIssues[groupTitle].map((issue, index: number) => ( {groupedByIssues[groupTitle].map((issue, index: number) => (
<Draggable <Draggable
key={issue.id} key={issue.id}
@ -124,7 +140,7 @@ export const SingleBoard: React.FC<Props> = ({
))} ))}
<span <span
style={{ style={{
display: orderBy === "manual" ? "inline" : "none", display: orderBy === "sort_order" ? "inline" : "none",
}} }}
> >
{provided.placeholder} {provided.placeholder}

View File

@ -49,7 +49,7 @@ type Props = {
editIssue: () => void; editIssue: () => void;
removeIssue?: (() => void) | null; removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
orderBy: NestedKeyOf<IIssue> | "manual" | null; orderBy: NestedKeyOf<IIssue> | null;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -150,7 +150,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
style: DraggingStyle | NotDraggingStyle | undefined, style: DraggingStyle | NotDraggingStyle | undefined,
snapshot: DraggableStateSnapshot snapshot: DraggableStateSnapshot
) { ) {
if (orderBy === "manual") return style; if (orderBy === "sort_order") return style;
if (!snapshot.isDragging) return {}; if (!snapshot.isDragging) return {};
if (!snapshot.isDropAnimating) { if (!snapshot.isDropAnimating) {
return style; return style;
@ -179,12 +179,13 @@ export const SingleBoardIssue: React.FC<Props> = ({
}); });
}); });
}; };
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
useEffect(() => { useEffect(() => {
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging); if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
}, [snapshot, handleTrashBox]); }, [snapshot, handleTrashBox]);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return ( return (
<div <div
className={`rounded border bg-white shadow-sm ${ className={`rounded border bg-white shadow-sm ${
@ -198,13 +199,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
<div className="group/card relative select-none p-2"> <div className="group/card relative select-none p-2">
{!isNotAllowed && ( {!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100"> <div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
{/* <button
type="button"
className="grid h-7 w-7 place-items-center rounded bg-white p-1 text-red-500 outline-none duration-300 hover:bg-red-50"
onClick={() => handleDeleteIssue(issue)}
>
<TrashIcon className="h-4 w-4" />
</button> */}
{type && !isNotAllowed && ( {type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis> <CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem> <CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>

View File

@ -130,7 +130,10 @@ export const IssuesFilterView: React.FC<Props> = ({ issues }) => {
option.key === "priority" ? null : ( option.key === "priority" ? null : (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={option.key} key={option.key}
onClick={() => setOrderBy(option.key)} onClick={() => {
console.log(option.key);
setOrderBy(option.key);
}}
> >
{option.name} {option.name}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>

View File

@ -67,7 +67,12 @@ export const IssuesView: React.FC<Props> = ({
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues); const {
issueView,
groupedByIssues,
groupByProperty: selectedGroup,
orderBy,
} = useIssueView(issues);
const { data: stateGroups } = useSWR( const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
@ -101,10 +106,25 @@ export const IssuesView: React.FC<Props> = ({
const { source, destination } = result; const { source, destination } = result;
const draggedItem = groupedByIssues[source.droppableId][source.index]; const draggedItem = groupedByIssues[source.droppableId][source.index];
let newSortOrder = draggedItem.sort_order;
if (destination.droppableId === "trashBox") { if (destination.droppableId === "trashBox") {
handleDeleteIssue(draggedItem); handleDeleteIssue(draggedItem);
} else { } else {
if (orderBy === "sort_order") {
const destinationGroupArray = groupedByIssues[destination.droppableId];
if (destination.index === 0) newSortOrder = destinationGroupArray[0].sort_order - 10000;
else if (destination.index === destinationGroupArray.length)
newSortOrder =
destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
else
newSortOrder =
(destinationGroupArray[destination.index - 1].sort_order +
destinationGroupArray[destination.index].sort_order) /
2;
}
if (source.droppableId !== destination.droppableId) { if (source.droppableId !== destination.droppableId) {
const sourceGroup = source.droppableId; // source group id const sourceGroup = source.droppableId; // source group id
const destinationGroup = destination.droppableId; // destination group id const destinationGroup = destination.droppableId; // destination group id
@ -127,6 +147,7 @@ export const IssuesView: React.FC<Props> = ({
issue_detail: { issue_detail: {
...draggedItem, ...draggedItem,
priority: destinationGroup, priority: destinationGroup,
sort_order: newSortOrder,
}, },
}; };
} }
@ -149,6 +170,7 @@ export const IssuesView: React.FC<Props> = ({
issue_detail: { issue_detail: {
...draggedItem, ...draggedItem,
priority: destinationGroup, priority: destinationGroup,
sort_order: newSortOrder,
}, },
}; };
} }
@ -169,6 +191,7 @@ export const IssuesView: React.FC<Props> = ({
return { return {
...draggedItem, ...draggedItem,
priority: destinationGroup, priority: destinationGroup,
sort_order: newSortOrder,
}; };
return issue; return issue;
@ -183,6 +206,7 @@ export const IssuesView: React.FC<Props> = ({
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
priority: destinationGroup, priority: destinationGroup,
sort_order: newSortOrder,
}) })
.then((res) => { .then((res) => {
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
@ -212,6 +236,7 @@ export const IssuesView: React.FC<Props> = ({
...draggedItem, ...draggedItem,
state_detail: destinationState, state_detail: destinationState,
state: destinationStateId, state: destinationStateId,
sort_order: newSortOrder,
}, },
}; };
} }
@ -235,6 +260,7 @@ export const IssuesView: React.FC<Props> = ({
...draggedItem, ...draggedItem,
state_detail: destinationState, state_detail: destinationState,
state: destinationStateId, state: destinationStateId,
sort_order: newSortOrder,
}, },
}; };
} }
@ -256,6 +282,7 @@ export const IssuesView: React.FC<Props> = ({
...draggedItem, ...draggedItem,
state_detail: destinationState, state_detail: destinationState,
state: destinationStateId, state: destinationStateId,
sort_order: newSortOrder,
}; };
return issue; return issue;
@ -270,6 +297,7 @@ export const IssuesView: React.FC<Props> = ({
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
state: destinationStateId, state: destinationStateId,
sort_order: newSortOrder,
}) })
.then((res) => { .then((res) => {
if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); if (cycleId) mutate(CYCLE_ISSUES(cycleId as string));
@ -288,6 +316,7 @@ export const IssuesView: React.FC<Props> = ({
groupedByIssues, groupedByIssues,
projectId, projectId,
selectedGroup, selectedGroup,
orderBy,
states, states,
handleDeleteIssue, handleDeleteIssue,
] ]

View File

@ -39,12 +39,18 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
.then((res) => { .then((res) => {
mutate(CYCLE_LIST(projectId as string)); mutate(CYCLE_LIST(projectId as string));
handleClose(); handleClose();
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle created successfully.",
});
}) })
.catch((err) => { .catch((err) => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error", title: "Error!",
message: "Error in creating cycle. Please try again!", message: "Error in creating cycle. Please try again.",
}); });
}); });
}; };
@ -55,12 +61,18 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
.then((res) => { .then((res) => {
mutate(CYCLE_LIST(projectId as string)); mutate(CYCLE_LIST(projectId as string));
handleClose(); handleClose();
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle updated successfully.",
});
}) })
.catch((err) => { .catch((err) => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error", title: "Error!",
message: "Error in updating cycle. Please try again!", message: "Error in updating cycle. Please try again.",
}); });
}); });
}; };

View File

@ -9,14 +9,12 @@ export const GROUP_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> |
{ name: "None", key: null }, { name: "None", key: null },
]; ];
export const ORDER_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | "manual" | null }> = export const ORDER_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
[ { name: "Manual", key: "sort_order" },
// { name: "Manual", key: "manual" },
{ name: "Last created", key: "created_at" }, { name: "Last created", key: "created_at" },
{ name: "Last updated", key: "updated_at" }, { name: "Last updated", key: "updated_at" },
{ name: "Priority", key: "priority" }, { name: "Priority", key: "priority" },
// { name: "None", key: null }, ];
];
export const FILTER_ISSUE_OPTIONS: Array<{ export const FILTER_ISSUE_OPTIONS: Array<{
name: string; name: string;

View File

@ -19,7 +19,7 @@ type IssueViewProps = {
issueView: "list" | "kanban" | null; issueView: "list" | "kanban" | null;
groupByProperty: NestedKeyOf<IIssue> | null; groupByProperty: NestedKeyOf<IIssue> | null;
filterIssue: "activeIssue" | "backlogIssue" | null; filterIssue: "activeIssue" | "backlogIssue" | null;
orderBy: NestedKeyOf<IIssue> | "manual" | null; orderBy: NestedKeyOf<IIssue> | null;
}; };
type ReducerActionType = { type ReducerActionType = {
@ -34,12 +34,12 @@ type ReducerActionType = {
}; };
type ContextType = { type ContextType = {
orderBy: NestedKeyOf<IIssue> | "manual" | null; orderBy: NestedKeyOf<IIssue> | null;
issueView: "list" | "kanban" | null; issueView: "list" | "kanban" | null;
groupByProperty: NestedKeyOf<IIssue> | null; groupByProperty: NestedKeyOf<IIssue> | null;
filterIssue: "activeIssue" | "backlogIssue" | null; filterIssue: "activeIssue" | "backlogIssue" | null;
setGroupByProperty: (property: NestedKeyOf<IIssue> | null) => void; setGroupByProperty: (property: NestedKeyOf<IIssue> | null) => void;
setOrderBy: (property: NestedKeyOf<IIssue> | "manual" | null) => void; setOrderBy: (property: NestedKeyOf<IIssue> | null) => void;
setFilterIssue: (property: "activeIssue" | "backlogIssue" | null) => void; setFilterIssue: (property: "activeIssue" | "backlogIssue" | null) => void;
resetFilterToDefault: () => void; resetFilterToDefault: () => void;
setNewFilterDefaultView: () => void; setNewFilterDefaultView: () => void;
@ -51,7 +51,7 @@ type StateType = {
issueView: "list" | "kanban" | null; issueView: "list" | "kanban" | null;
groupByProperty: NestedKeyOf<IIssue> | null; groupByProperty: NestedKeyOf<IIssue> | null;
filterIssue: "activeIssue" | "backlogIssue" | null; filterIssue: "activeIssue" | "backlogIssue" | null;
orderBy: NestedKeyOf<IIssue> | "manual" | null; orderBy: NestedKeyOf<IIssue> | null;
}; };
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType; type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
@ -220,7 +220,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
); );
const setOrderBy = useCallback( const setOrderBy = useCallback(
(property: NestedKeyOf<IIssue> | "manual" | null) => { (property: NestedKeyOf<IIssue> | null) => {
dispatch({ dispatch({
type: "SET_ORDER_BY_PROPERTY", type: "SET_ORDER_BY_PROPERTY",
payload: { payload: {

View File

@ -97,7 +97,7 @@ const useIssueView = (projectIssues: IIssue[]) => {
groupedByIssues = Object.fromEntries( groupedByIssues = Object.fromEntries(
Object.entries(groupedByIssues).map(([key, value]) => [ Object.entries(groupedByIssues).map(([key, value]) => [
key, key,
orderArrayBy(value, orderBy, "descending"), orderArrayBy(value, orderBy, orderBy === "sort_order" ? "ascending" : "descending"),
]) ])
); );
} }

View File

@ -96,6 +96,7 @@ export interface IIssue {
project: string; project: string;
project_detail: IProject; project_detail: IProject;
sequence_id: number; sequence_id: number;
sort_order: number;
sprints: string | null; sprints: string | null;
start_date: string | null; start_date: string | null;
state: string; state: string;