mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into preview
This commit is contained in:
commit
c950e3d3f7
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "plane-api",
|
"name": "plane-api",
|
||||||
"version": "0.15.1"
|
"version": "0.16.0"
|
||||||
}
|
}
|
||||||
|
@ -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):
|
||||||
|
@ -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,
|
||||||
|
@ -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": [
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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": {
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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" : ""}`}>
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
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;
|
||||||
|
};
|
@ -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}
|
||||||
|
@ -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",
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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> {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user