forked from github/plane
feat: manual ordering of issues (#305)
This commit is contained in:
parent
202096500e
commit
818fe3ecf7
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
]
|
]
|
||||||
|
@ -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.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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: {
|
||||||
|
@ -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"),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
1
apps/app/types/issues.d.ts
vendored
1
apps/app/types/issues.d.ts
vendored
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user