From 01e09873e68f695519c32244f53222bb8bd68ab9 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Mon, 26 Feb 2024 19:42:36 +0530 Subject: [PATCH] [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 Co-authored-by: Anmol Singh Bhatia --- apiserver/plane/app/views/project.py | 17 +++--- web/components/project/sidebar-list-item.tsx | 5 +- web/components/project/sidebar-list.tsx | 34 +++++++---- web/helpers/project.helper.ts | 45 ++++++++++++++ web/lib/app-provider.tsx | 2 +- web/services/project/project.service.ts | 3 - web/store/project/project.store.ts | 63 +++++++------------- yarn.lock | 2 +- 8 files changed, 99 insertions(+), 72 deletions(-) create mode 100644 web/helpers/project.helper.ts diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 5d2f95673..6f9b2618e 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -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, diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index f899a9b31..79d632c1e 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -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 = 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 = observer((props) => { snapshot?.isDragging ? "opacity-60" : "" } ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`} > - {provided && ( + {provided && !disableDrag && ( { // states @@ -32,9 +34,9 @@ export const ProjectSidebarList: FC = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); const { + getProjectById, joinedProjectIds: joinedProjects, favoriteProjectIds: favoriteProjects, - orderProjectsWithSortOrder, updateProjectView, } = useProject(); // router @@ -55,22 +57,27 @@ 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); - - updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Something went wrong. Please try again.", - }); + 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", + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); }; const isCollapsed = sidebarCollapsed || false; @@ -176,6 +183,7 @@ export const ProjectSidebarList: FC = observer(() => { snapshot={snapshot} handleCopyText={() => handleCopyText(projectId)} shortContextMenu + disableDrag /> )} diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts new file mode 100644 index 000000000..8d78964ee --- /dev/null +++ b/web/helpers/project.helper.ts @@ -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; +}; diff --git a/web/lib/app-provider.tsx b/web/lib/app-provider.tsx index 64d323cf0..9c06947af 100644 --- a/web/lib/app-provider.tsx +++ b/web/lib/app-provider.tsx @@ -50,7 +50,7 @@ export const AppProvider: FC = observer((props) => { { diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index ef88c5b0f..a47d459f8 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -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; removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise; - // project-order action - orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number; // project-view action updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise; // 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; } diff --git a/yarn.lock b/yarn.lock index fc53aade6..f413d1a44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==