[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:
guru_sainath 2024-02-26 19:42:36 +05:30 committed by GitHub
parent dad682a7c3
commit 01e09873e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 99 additions and 72 deletions

View File

@ -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,

View File

@ -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"

View File

@ -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>
)}

View 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;
};

View File

@ -50,7 +50,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
<CrispWrapper user={currentUser}>
<PostHogProvider
user={currentUser}
currentWorkspaceId= {currentWorkspace?.id}
currentWorkspaceId={currentWorkspace?.id}
workspaceRole={currentWorkspaceRole}
projectRole={currentProjectRole}
posthogAPIKey={envConfig?.posthog_api_key || null}

View File

@ -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> {

View File

@ -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;
}

View File

@ -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==