mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-534] fix: application sidebar project, favourite project sidebar DND (#3778)
* fix: application sidebar project and favorite project reordering issue resolved * fix: enabled posthog * chore: project sort order in POST * chore: project member sort order * chore: project sorting order * fix: handle dragdrop functionality from store to helper function in project sidebar * fix: resolved build error --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
parent
dad682a7c3
commit
01e09873e6
@ -77,6 +77,12 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
sort_order = ProjectMember.objects.filter(
|
||||
member=self.request.user,
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
is_active=True,
|
||||
).values("sort_order")
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
@ -147,6 +153,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(sort_order=Subquery(sort_order))
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
@ -166,16 +173,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
|
||||
sort_order_query = ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
is_active=True,
|
||||
).values("sort_order")
|
||||
projects = (
|
||||
self.get_queryset()
|
||||
.annotate(sort_order=Subquery(sort_order_query))
|
||||
.order_by("sort_order", "name")
|
||||
)
|
||||
if request.GET.get("per_page", False) and request.GET.get(
|
||||
@ -204,7 +203,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
serializer.save()
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
project_member = ProjectMember.objects.create(
|
||||
_ = ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member=request.user,
|
||||
role=20,
|
||||
|
@ -37,6 +37,7 @@ type Props = {
|
||||
snapshot?: DraggableStateSnapshot;
|
||||
handleCopyText: () => void;
|
||||
shortContextMenu?: boolean;
|
||||
disableDrag?: boolean;
|
||||
};
|
||||
|
||||
const navigation = (workspaceSlug: string, projectId: string) => [
|
||||
@ -79,7 +80,7 @@ const navigation = (workspaceSlug: string, projectId: string) => [
|
||||
|
||||
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { projectId, provided, snapshot, handleCopyText, shortContextMenu = false } = props;
|
||||
const { projectId, provided, snapshot, handleCopyText, shortContextMenu = false, disableDrag } = props;
|
||||
// store hooks
|
||||
const { theme: themeStore } = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
@ -163,7 +164,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
snapshot?.isDragging ? "opacity-60" : ""
|
||||
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
||||
>
|
||||
{provided && (
|
||||
{provided && !disableDrag && (
|
||||
<Tooltip
|
||||
tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
|
||||
position="top-right"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, FC, useRef, useEffect } from "react";
|
||||
import { useState, FC, useRef, useEffect, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
@ -11,9 +11,11 @@ import useToast from "hooks/use-toast";
|
||||
import { CreateProjectModal, ProjectSidebarListItem } from "components/project";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
import { orderJoinedProjects } from "helpers/project.helper";
|
||||
import { cn } from "helpers/common.helper";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { IProject } from "@plane/types";
|
||||
|
||||
export const ProjectSidebarList: FC = observer(() => {
|
||||
// states
|
||||
@ -32,9 +34,9 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const {
|
||||
getProjectById,
|
||||
joinedProjectIds: joinedProjects,
|
||||
favoriteProjectIds: favoriteProjects,
|
||||
orderProjectsWithSortOrder,
|
||||
updateProjectView,
|
||||
} = useProject();
|
||||
// router
|
||||
@ -55,15 +57,20 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const onDragEnd = async (result: DropResult) => {
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
const { source, destination, draggableId } = result;
|
||||
|
||||
if (!destination || !workspaceSlug) return;
|
||||
|
||||
if (source.index === destination.index) return;
|
||||
|
||||
const updatedSortOrder = orderProjectsWithSortOrder(source.index, destination.index, draggableId);
|
||||
const joinedProjectsList: IProject[] = [];
|
||||
joinedProjects.map((projectId) => {
|
||||
const _project = getProjectById(projectId);
|
||||
if (_project) joinedProjectsList.push(_project);
|
||||
});
|
||||
if (joinedProjectsList.length <= 0) return;
|
||||
|
||||
const updatedSortOrder = orderJoinedProjects(source.index, destination.index, draggableId, joinedProjectsList);
|
||||
if (updatedSortOrder != undefined)
|
||||
updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@ -176,6 +183,7 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
snapshot={snapshot}
|
||||
handleCopyText={() => handleCopyText(projectId)}
|
||||
shortContextMenu
|
||||
disableDrag
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
45
web/helpers/project.helper.ts
Normal file
45
web/helpers/project.helper.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { IProject } from "@plane/types";
|
||||
|
||||
/**
|
||||
* Updates the sort order of the project.
|
||||
* @param sortIndex
|
||||
* @param destinationIndex
|
||||
* @param projectId
|
||||
* @returns number | undefined
|
||||
*/
|
||||
export const orderJoinedProjects = (
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
currentProjectId: string,
|
||||
joinedProjects: IProject[]
|
||||
): number | undefined => {
|
||||
if (!currentProjectId || sourceIndex < 0 || destinationIndex < 0 || joinedProjects.length <= 0) return undefined;
|
||||
|
||||
let updatedSortOrder: number | undefined = undefined;
|
||||
const sortOrderDefaultValue = 10000;
|
||||
|
||||
if (destinationIndex === 0) {
|
||||
// updating project at the top of the project
|
||||
const currentSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||
updatedSortOrder = currentSortOrder - sortOrderDefaultValue;
|
||||
} else if (destinationIndex === joinedProjects.length - 1) {
|
||||
// updating project at the bottom of the project
|
||||
const currentSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
|
||||
updatedSortOrder = currentSortOrder + sortOrderDefaultValue;
|
||||
} else {
|
||||
// updating project in the middle of the project
|
||||
if (sourceIndex > destinationIndex) {
|
||||
const destinationTopProjectSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
|
||||
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
|
||||
updatedSortOrder = updatedValue;
|
||||
} else {
|
||||
const destinationTopProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex + 1].sort_order || 0;
|
||||
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
|
||||
updatedSortOrder = updatedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedSortOrder;
|
||||
};
|
@ -72,9 +72,6 @@ export class ProjectService extends APIService {
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: {
|
||||
view_props?: IProjectViewProps;
|
||||
default_props?: IProjectViewProps;
|
||||
preferences?: ProjectPreferences;
|
||||
sort_order?: number;
|
||||
}
|
||||
): Promise<any> {
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import set from "lodash/set";
|
||||
import sortBy from "lodash/sortBy";
|
||||
// types
|
||||
import { RootStore } from "../root.store";
|
||||
import { IProject } from "@plane/types";
|
||||
// services
|
||||
import { IssueLabelService, IssueService } from "services/issue";
|
||||
import { ProjectService, ProjectStateService } from "services/project";
|
||||
import { cloneDeep, update } from "lodash";
|
||||
export interface IProjectStore {
|
||||
// observables
|
||||
searchQuery: string;
|
||||
@ -28,8 +30,6 @@ export interface IProjectStore {
|
||||
// favorites actions
|
||||
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||
// project-order action
|
||||
orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number;
|
||||
// project-view action
|
||||
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
|
||||
// CRUD actions
|
||||
@ -71,8 +71,6 @@ export class ProjectStore implements IProjectStore {
|
||||
// favorites actions
|
||||
addProjectToFavorites: action,
|
||||
removeProjectFromFavorites: action,
|
||||
// project-order action
|
||||
orderProjectsWithSortOrder: action,
|
||||
// project-view action
|
||||
updateProjectView: action,
|
||||
// CRUD actions
|
||||
@ -128,7 +126,11 @@ export class ProjectStore implements IProjectStore {
|
||||
get joinedProjectIds() {
|
||||
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
|
||||
if (!currentWorkspace) return [];
|
||||
const projectIds = Object.values(this.projectMap ?? {})
|
||||
|
||||
let projects = Object.values(this.projectMap ?? {});
|
||||
projects = sortBy(projects, "sort_order");
|
||||
|
||||
const projectIds = projects
|
||||
.filter((project) => project.workspace === currentWorkspace.id && project.is_member)
|
||||
.map((project) => project.id);
|
||||
return projectIds;
|
||||
@ -140,7 +142,11 @@ export class ProjectStore implements IProjectStore {
|
||||
get favoriteProjectIds() {
|
||||
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
|
||||
if (!currentWorkspace) return [];
|
||||
const projectIds = Object.values(this.projectMap ?? {})
|
||||
|
||||
let projects = Object.values(this.projectMap ?? {});
|
||||
projects = sortBy(projects, "created_at");
|
||||
|
||||
const projectIds = projects
|
||||
.filter((project) => project.workspace === currentWorkspace.id && project.is_favorite)
|
||||
.map((project) => project.id);
|
||||
return projectIds;
|
||||
@ -253,41 +259,6 @@ export class ProjectStore implements IProjectStore {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the sort order of the project.
|
||||
* @param sortIndex
|
||||
* @param destinationIndex
|
||||
* @param projectId
|
||||
* @returns
|
||||
*/
|
||||
orderProjectsWithSortOrder = (sortIndex: number, destinationIndex: number, projectId: string) => {
|
||||
try {
|
||||
const workspaceSlug = this.rootStore.app.router.workspaceSlug;
|
||||
if (!workspaceSlug) return 0;
|
||||
const projectsList = Object.values(this.projectMap || {}) || [];
|
||||
let updatedSortOrder = projectsList[sortIndex].sort_order;
|
||||
if (destinationIndex === 0) updatedSortOrder = (projectsList[0].sort_order as number) - 1000;
|
||||
else if (destinationIndex === projectsList.length - 1)
|
||||
updatedSortOrder = (projectsList[projectsList.length - 1].sort_order as number) + 1000;
|
||||
else {
|
||||
const destinationSortingOrder = projectsList[destinationIndex].sort_order as number;
|
||||
const relativeDestinationSortingOrder =
|
||||
sortIndex < destinationIndex
|
||||
? (projectsList[destinationIndex + 1].sort_order as number)
|
||||
: (projectsList[destinationIndex - 1].sort_order as number);
|
||||
|
||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
||||
}
|
||||
runInAction(() => {
|
||||
set(this.projectMap, [projectId, "sort_order"], updatedSortOrder);
|
||||
});
|
||||
return updatedSortOrder;
|
||||
} catch (error) {
|
||||
console.log("failed to update sort order of the projects");
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the project view
|
||||
* @param workspaceSlug
|
||||
@ -295,12 +266,18 @@ export class ProjectStore implements IProjectStore {
|
||||
* @param viewProps
|
||||
* @returns
|
||||
*/
|
||||
updateProjectView = async (workspaceSlug: string, projectId: string, viewProps: any) => {
|
||||
updateProjectView = async (workspaceSlug: string, projectId: string, viewProps: { sort_order: number }) => {
|
||||
const currentProjectSortOrder = this.getProjectById(projectId)?.sort_order;
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.projectMap, [projectId, "sort_order"], viewProps?.sort_order);
|
||||
});
|
||||
const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps);
|
||||
await this.fetchProjects(workspaceSlug);
|
||||
return response;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
set(this.projectMap, [projectId, "sort_order"], currentProjectSortOrder);
|
||||
});
|
||||
console.log("Failed to update sort order of the projects");
|
||||
throw error;
|
||||
}
|
||||
|
@ -2812,7 +2812,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18.2.42":
|
||||
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
|
||||
version "18.2.42"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
||||
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
||||
|
Loading…
Reference in New Issue
Block a user