Merge branch 'develop' of github.com:makeplane/plane into preview

This commit is contained in:
sriram veeraghanta 2024-02-26 19:48:12 +05:30
commit c950e3d3f7
33 changed files with 181 additions and 92 deletions

View File

@ -1,4 +1,4 @@
{ {
"name": "plane-api", "name": "plane-api",
"version": "0.15.1" "version": "0.16.0"
} }

View File

@ -33,6 +33,7 @@ from plane.app.serializers import (
IssueSerializer, IssueSerializer,
InboxSerializer, InboxSerializer,
InboxIssueSerializer, InboxIssueSerializer,
IssueDetailSerializer,
) )
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
@ -426,11 +427,10 @@ class InboxIssueViewSet(BaseViewSet):
) )
) )
).first() ).first()
if issue is None: if issue is None:
return Response({"error": "Requested object was not found"}, status=status.HTTP_404_NOT_FOUND) return Response({"error": "Requested object was not found"}, status=status.HTTP_404_NOT_FOUND)
serializer = IssueSerializer(issue) serializer = IssueDetailSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, issue_id): def destroy(self, request, slug, project_id, inbox_id, issue_id):

View File

@ -77,6 +77,12 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
] ]
def get_queryset(self): 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( return self.filter_queryset(
super() super()
.get_queryset() .get_queryset()
@ -147,6 +153,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
) )
) )
) )
.annotate(sort_order=Subquery(sort_order))
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"project_projectmember", "project_projectmember",
@ -166,16 +173,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
for field in request.GET.get("fields", "").split(",") for field in request.GET.get("fields", "").split(",")
if field 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 = ( projects = (
self.get_queryset() self.get_queryset()
.annotate(sort_order=Subquery(sort_order_query))
.order_by("sort_order", "name") .order_by("sort_order", "name")
) )
if request.GET.get("per_page", False) and request.GET.get( if request.GET.get("per_page", False) and request.GET.get(
@ -204,7 +203,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer.save() serializer.save()
# Add the user as Administrator to the project # Add the user as Administrator to the project
project_member = ProjectMember.objects.create( _ = ProjectMember.objects.create(
project_id=serializer.data["id"], project_id=serializer.data["id"],
member=request.user, member=request.user,
role=20, role=20,

View File

@ -1,6 +1,6 @@
{ {
"repository": "https://github.com/makeplane/plane.git", "repository": "https://github.com/makeplane/plane.git",
"version": "0.15.1", "version": "0.16.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"workspaces": [ "workspaces": [

View File

@ -1,6 +1,6 @@
{ {
"name": "@plane/editor-core", "name": "@plane/editor-core",
"version": "0.15.1", "version": "0.16.0",
"description": "Core Editor that powers Plane", "description": "Core Editor that powers Plane",
"private": true, "private": true,
"main": "./dist/index.mjs", "main": "./dist/index.mjs",

View File

@ -1,6 +1,6 @@
{ {
"name": "@plane/document-editor", "name": "@plane/document-editor",
"version": "0.15.1", "version": "0.16.0",
"description": "Package that powers Plane's Pages Editor", "description": "Package that powers Plane's Pages Editor",
"main": "./dist/index.mjs", "main": "./dist/index.mjs",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",

View File

@ -1,6 +1,6 @@
{ {
"name": "@plane/editor-extensions", "name": "@plane/editor-extensions",
"version": "0.15.1", "version": "0.16.0",
"description": "Package that powers Plane's Editor with extensions", "description": "Package that powers Plane's Editor with extensions",
"private": true, "private": true,
"main": "./dist/index.mjs", "main": "./dist/index.mjs",

View File

@ -1,6 +1,6 @@
{ {
"name": "@plane/lite-text-editor", "name": "@plane/lite-text-editor",
"version": "0.15.1", "version": "0.16.0",
"description": "Package that powers Plane's Comment Editor", "description": "Package that powers Plane's Comment Editor",
"private": true, "private": true,
"main": "./dist/index.mjs", "main": "./dist/index.mjs",

View File

@ -1,6 +1,6 @@
{ {
"name": "@plane/rich-text-editor", "name": "@plane/rich-text-editor",
"version": "0.15.1", "version": "0.16.0",
"description": "Rich Text Editor that powers Plane", "description": "Rich Text Editor that powers Plane",
"private": true, "private": true,
"main": "./dist/index.mjs", "main": "./dist/index.mjs",

View File

@ -1,7 +1,7 @@
{ {
"name": "eslint-config-custom", "name": "eslint-config-custom",
"private": true, "private": true,
"version": "0.15.1", "version": "0.16.0",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "tailwind-config-custom", "name": "tailwind-config-custom",
"version": "0.15.1", "version": "0.16.0",
"description": "common tailwind configuration across monorepo", "description": "common tailwind configuration across monorepo",
"main": "index.js", "main": "index.js",
"private": true, "private": true,

View File

@ -1,6 +1,6 @@
{ {
"name": "tsconfig", "name": "tsconfig",
"version": "0.15.1", "version": "0.16.0",
"private": true, "private": true,
"files": [ "files": [
"base.json", "base.json",

View File

@ -1,6 +1,6 @@
{ {
"name": "@plane/types", "name": "@plane/types",
"version": "0.15.1", "version": "0.16.0",
"private": true, "private": true,
"main": "./src/index.d.ts" "main": "./src/index.d.ts"
} }

View File

@ -2,7 +2,7 @@
"name": "@plane/ui", "name": "@plane/ui",
"description": "UI components shared across multiple apps internally", "description": "UI components shared across multiple apps internally",
"private": true, "private": true,
"version": "0.15.1", "version": "0.16.0",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "space", "name": "space",
"version": "0.15.1", "version": "0.16.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "turbo run develop", "dev": "turbo run develop",

View File

@ -148,6 +148,13 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
toggleDropdown(); toggleDropdown();
}; };
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose); useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => { useEffect(() => {
@ -231,6 +238,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search" placeholder="Search"
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/> />
</div> </div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll"> <div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">

View File

@ -137,6 +137,13 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
toggleDropdown(); toggleDropdown();
}; };
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose); useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => { useEffect(() => {
@ -217,6 +224,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search" placeholder="Search"
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/> />
</div> </div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll"> <div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">

View File

@ -130,6 +130,13 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
toggleDropdown(); toggleDropdown();
}; };
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose); useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => { useEffect(() => {
@ -215,6 +222,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search" placeholder="Search"
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/> />
</div> </div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll"> <div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">

View File

@ -249,6 +249,13 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
toggleDropdown(); toggleDropdown();
}; };
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose); useOutsideClickDetector(dropdownRef, handleClose);
const comboboxProps: any = { const comboboxProps: any = {
@ -349,6 +356,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search" placeholder="Search"
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/> />
</div> </div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll"> <div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">

View File

@ -329,6 +329,13 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
toggleDropdown(); toggleDropdown();
}; };
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose); useOutsideClickDetector(dropdownRef, handleClose);
const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant) const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
@ -417,6 +424,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search" placeholder="Search"
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/> />
</div> </div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll"> <div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">

View File

@ -119,6 +119,13 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
toggleDropdown(); toggleDropdown();
}; };
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
useOutsideClickDetector(dropdownRef, handleClose); useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => { useEffect(() => {
@ -205,6 +212,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search" placeholder="Search"
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/> />
</div> </div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll"> <div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">

View File

@ -80,6 +80,13 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
</div> </div>
); );
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (query !== "" && e.key === "Escape") {
e.stopPropagation();
setQuery("");
}
};
if (!issue) return <></>; if (!issue) return <></>;
return ( return (
@ -118,6 +125,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search" placeholder="Search"
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/> />
</div> </div>
</div> </div>

View File

@ -360,7 +360,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
/> />
</div> </div>
<div <div
className="h-full w-full sm:w-1/2 md:w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5 fixed md:relative bg-custom-sidebar-background-100 right-0 z-[5]" className="h-full w-full min-w-[300px] lg:min-w-80 xl:min-w-96 sm:w-1/2 md:w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5 fixed md:relative bg-custom-sidebar-background-100 right-0 z-[5]"
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}} style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
> >
<IssueDetailsSidebar <IssueDetailsSidebar

View File

@ -133,7 +133,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</div> </div>
</div> </div>
<div className="h-full w-full overflow-y-auto px-5"> <div className="h-full w-full overflow-y-auto px-6">
<h5 className="text-sm font-medium mt-6">Properties</h5> <h5 className="text-sm font-medium mt-6">Properties</h5>
{/* TODO: render properties using a common component */} {/* TODO: render properties using a common component */}
<div className={`mt-3 mb-2 space-y-2.5 ${!is_editable ? "opacity-60" : ""}`}> <div className={`mt-3 mb-2 space-y-2.5 ${!is_editable ? "opacity-60" : ""}`}>

View File

@ -67,7 +67,7 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
> >
{loading ? ( {loading ? (
<span> <span>
<span className="hidden sm:block">Loading</span>... <span className="hidden sm:block">Loading...</span>
</span> </span>
) : isSubscribed ? ( ) : isSubscribed ? (
<div className="hidden sm:block">Unsubscribe</div> <div className="hidden sm:block">Unsubscribe</div>

View File

@ -37,6 +37,7 @@ type Props = {
snapshot?: DraggableStateSnapshot; snapshot?: DraggableStateSnapshot;
handleCopyText: () => void; handleCopyText: () => void;
shortContextMenu?: boolean; shortContextMenu?: boolean;
disableDrag?: boolean;
}; };
const navigation = (workspaceSlug: string, projectId: string) => [ const navigation = (workspaceSlug: string, projectId: string) => [
@ -79,7 +80,7 @@ const navigation = (workspaceSlug: string, projectId: string) => [
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => { export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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 // store hooks
const { theme: themeStore } = useApplication(); const { theme: themeStore } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
@ -163,7 +164,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
snapshot?.isDragging ? "opacity-60" : "" snapshot?.isDragging ? "opacity-60" : ""
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`} } ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
> >
{provided && ( {provided && !disableDrag && (
<Tooltip <Tooltip
tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"} tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
position="top-right" 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 { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
@ -11,9 +11,11 @@ import useToast from "hooks/use-toast";
import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; import { CreateProjectModal, ProjectSidebarListItem } from "components/project";
// helpers // helpers
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
import { orderJoinedProjects } from "helpers/project.helper";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
// constants // constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { IProject } from "@plane/types";
export const ProjectSidebarList: FC = observer(() => { export const ProjectSidebarList: FC = observer(() => {
// states // states
@ -32,9 +34,9 @@ export const ProjectSidebarList: FC = observer(() => {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { const {
getProjectById,
joinedProjectIds: joinedProjects, joinedProjectIds: joinedProjects,
favoriteProjectIds: favoriteProjects, favoriteProjectIds: favoriteProjects,
orderProjectsWithSortOrder,
updateProjectView, updateProjectView,
} = useProject(); } = useProject();
// router // router
@ -55,15 +57,20 @@ export const ProjectSidebarList: FC = observer(() => {
}); });
}; };
const onDragEnd = async (result: DropResult) => { const onDragEnd = (result: DropResult) => {
const { source, destination, draggableId } = result; const { source, destination, draggableId } = result;
if (!destination || !workspaceSlug) return; if (!destination || !workspaceSlug) return;
if (source.index === destination.index) 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(() => { updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
@ -176,6 +183,7 @@ export const ProjectSidebarList: FC = observer(() => {
snapshot={snapshot} snapshot={snapshot}
handleCopyText={() => handleCopyText(projectId)} handleCopyText={() => handleCopyText(projectId)}
shortContextMenu shortContextMenu
disableDrag
/> />
</div> </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}> <CrispWrapper user={currentUser}>
<PostHogProvider <PostHogProvider
user={currentUser} user={currentUser}
currentWorkspaceId= {currentWorkspace?.id} currentWorkspaceId={currentWorkspace?.id}
workspaceRole={currentWorkspaceRole} workspaceRole={currentWorkspaceRole}
projectRole={currentProjectRole} projectRole={currentProjectRole}
posthogAPIKey={envConfig?.posthog_api_key || null} posthogAPIKey={envConfig?.posthog_api_key || null}

View File

@ -1,6 +1,6 @@
{ {
"name": "web", "name": "web",
"version": "0.15.1", "version": "0.16.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "turbo run develop", "dev": "turbo run develop",

View File

@ -7,6 +7,7 @@ import { useProject, useInboxIssues } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { InboxLayoutLoader } from "components/ui";
import { PageHead } from "components/core"; import { PageHead } from "components/core";
import { ProjectInboxHeader } from "components/headers"; import { ProjectInboxHeader } from "components/headers";
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
@ -23,7 +24,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
issues: { fetchInboxIssues }, issues: { fetchInboxIssues },
} = useInboxIssues(); } = useInboxIssues();
// fetching the Inbox filters and issues // fetching the Inbox filters and issues
useSWR( const { isLoading } = useSWR(
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` ? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
: null, : null,
@ -37,7 +38,12 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
// derived values // derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : undefined; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : undefined;
if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>; if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view || isLoading)
return (
<div className="flex h-full flex-col">
<InboxLayoutLoader />
</div>
);
return ( return (
<> <>

View File

@ -72,9 +72,6 @@ export class ProjectService extends APIService {
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
data: { data: {
view_props?: IProjectViewProps;
default_props?: IProjectViewProps;
preferences?: ProjectPreferences;
sort_order?: number; sort_order?: number;
} }
): Promise<any> { ): Promise<any> {

View File

@ -1,12 +1,14 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx"; import { observable, action, computed, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
import set from "lodash/set"; import set from "lodash/set";
import sortBy from "lodash/sortBy";
// types // types
import { RootStore } from "../root.store"; import { RootStore } from "../root.store";
import { IProject } from "@plane/types"; import { IProject } from "@plane/types";
// services // services
import { IssueLabelService, IssueService } from "services/issue"; import { IssueLabelService, IssueService } from "services/issue";
import { ProjectService, ProjectStateService } from "services/project"; import { ProjectService, ProjectStateService } from "services/project";
import { cloneDeep, update } from "lodash";
export interface IProjectStore { export interface IProjectStore {
// observables // observables
searchQuery: string; searchQuery: string;
@ -28,8 +30,6 @@ export interface IProjectStore {
// favorites actions // favorites actions
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>; addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
removeProjectFromFavorites: (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 // project-view action
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>; updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
// CRUD actions // CRUD actions
@ -71,8 +71,6 @@ export class ProjectStore implements IProjectStore {
// favorites actions // favorites actions
addProjectToFavorites: action, addProjectToFavorites: action,
removeProjectFromFavorites: action, removeProjectFromFavorites: action,
// project-order action
orderProjectsWithSortOrder: action,
// project-view action // project-view action
updateProjectView: action, updateProjectView: action,
// CRUD actions // CRUD actions
@ -128,7 +126,11 @@ export class ProjectStore implements IProjectStore {
get joinedProjectIds() { get joinedProjectIds() {
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace; const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
if (!currentWorkspace) return []; 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) .filter((project) => project.workspace === currentWorkspace.id && project.is_member)
.map((project) => project.id); .map((project) => project.id);
return projectIds; return projectIds;
@ -140,7 +142,11 @@ export class ProjectStore implements IProjectStore {
get favoriteProjectIds() { get favoriteProjectIds() {
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace; const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
if (!currentWorkspace) return []; 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) .filter((project) => project.workspace === currentWorkspace.id && project.is_favorite)
.map((project) => project.id); .map((project) => project.id);
return projectIds; 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 * Updates the project view
* @param workspaceSlug * @param workspaceSlug
@ -295,12 +266,18 @@ export class ProjectStore implements IProjectStore {
* @param viewProps * @param viewProps
* @returns * @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 { try {
runInAction(() => {
set(this.projectMap, [projectId, "sort_order"], viewProps?.sort_order);
});
const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps); const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps);
await this.fetchProjects(workspaceSlug);
return response; return response;
} catch (error) { } catch (error) {
runInAction(() => {
set(this.projectMap, [projectId, "sort_order"], currentProjectSortOrder);
});
console.log("Failed to update sort order of the projects"); console.log("Failed to update sort order of the projects");
throw error; throw error;
} }