diff --git a/apiserver/plane/app/serializers/page.py b/apiserver/plane/app/serializers/page.py
index 853d854b3..6e38537f0 100644
--- a/apiserver/plane/app/serializers/page.py
+++ b/apiserver/plane/app/serializers/page.py
@@ -20,7 +20,15 @@ class PageSerializer(BaseSerializer):
write_only=True,
required=False,
)
- project = serializers.UUIDField(read_only=True)
+ # Many to many
+ label_ids = serializers.ListField(
+ child=serializers.UUIDField(),
+ required=False,
+ )
+ project_ids = serializers.ListField(
+ child=serializers.UUIDField(),
+ required=False,
+ )
class Meta:
model = Page
@@ -42,18 +50,14 @@ class PageSerializer(BaseSerializer):
"updated_by",
"view_props",
"logo_props",
- "project",
+ "label_ids",
+ "project_ids",
]
read_only_fields = [
"workspace",
"owned_by",
]
- def to_representation(self, instance):
- data = super().to_representation(instance)
- data["labels"] = [str(label.id) for label in instance.labels.all()]
- return data
-
def create(self, validated_data):
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
@@ -63,6 +67,7 @@ class PageSerializer(BaseSerializer):
# Get the workspace id from the project
project = Project.objects.get(pk=project_id)
+ # Create the page
page = Page.objects.create(
**validated_data,
description_html=description_html,
@@ -70,6 +75,7 @@ class PageSerializer(BaseSerializer):
workspace_id=project.workspace_id,
)
+ # Create the project page
ProjectPage.objects.create(
workspace_id=page.workspace_id,
project_id=project_id,
@@ -78,6 +84,7 @@ class PageSerializer(BaseSerializer):
updated_by_id=page.updated_by_id,
)
+ # Create page labels
if labels is not None:
PageLabel.objects.bulk_create(
[
diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py
index 580901879..0f2104187 100644
--- a/apiserver/plane/app/views/__init__.py
+++ b/apiserver/plane/app/views/__init__.py
@@ -180,7 +180,8 @@ from .page.base import (
PagesDescriptionViewSet,
)
-from .search import GlobalSearchEndpoint, IssueSearchEndpoint
+from .search.base import GlobalSearchEndpoint
+from .search.issue import IssueSearchEndpoint
from .external.base import (
diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py
index dbe85f2bc..9db2a4cf0 100644
--- a/apiserver/plane/app/views/page/base.py
+++ b/apiserver/plane/app/views/page/base.py
@@ -6,10 +6,13 @@ from django.core.serializers.json import DjangoJSONEncoder
# Django imports
from django.db import connection
-from django.db.models import Exists, OuterRef, Q, Subquery
+from django.db.models import Exists, OuterRef, Q, Value, UUIDField
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.http import StreamingHttpResponse
+from django.contrib.postgres.aggregates import ArrayAgg
+from django.contrib.postgres.fields import ArrayField
+from django.db.models.functions import Coalesce
# Third party imports
from rest_framework import status
@@ -70,9 +73,6 @@ class PageViewSet(BaseViewSet):
entity_identifier=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
- project_subquery = ProjectPage.objects.filter(
- page_id=OuterRef("id"), project_id=self.kwargs.get("project_id")
- ).values_list("project_id", flat=True)[:1]
return self.filter_queryset(
super()
.get_queryset()
@@ -91,8 +91,33 @@ class PageViewSet(BaseViewSet):
.order_by(self.request.GET.get("order_by", "-created_at"))
.prefetch_related("labels")
.order_by("-is_favorite", "-created_at")
- .annotate(project=Subquery(project_subquery))
- .filter(project=self.kwargs.get("project_id"))
+ .annotate(
+ project=Exists(
+ ProjectPage.objects.filter(
+ page_id=OuterRef("id"),
+ project_id=self.kwargs.get("project_id"),
+ )
+ )
+ )
+ .annotate(
+ label_ids=Coalesce(
+ ArrayAgg(
+ "page_labels__label_id",
+ distinct=True,
+ filter=~Q(page_labels__label_id__isnull=True),
+ ),
+ Value([], output_field=ArrayField(UUIDField())),
+ ),
+ project_ids=Coalesce(
+ ArrayAgg(
+ "projects__id",
+ distinct=True,
+ filter=~Q(projects__id=True),
+ ),
+ Value([], output_field=ArrayField(UUIDField())),
+ ),
+ )
+ .filter(project=True)
.distinct()
)
@@ -112,7 +137,7 @@ class PageViewSet(BaseViewSet):
serializer.save()
# capture the page transaction
page_transaction.delay(request.data, None, serializer.data["id"])
- page = Page.objects.get(pk=serializer.data["id"])
+ page = self.get_queryset().get(pk=serializer.data["id"])
serializer = PageDetailSerializer(page)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -302,7 +327,7 @@ class PageViewSet(BaseViewSet):
# remove parent from all the children
_ = Page.objects.filter(
- parent_id=pk, project_id=project_id, workspace__slug=slug
+ parent_id=pk, projects__id=project_id, workspace__slug=slug
).update(parent=None)
page.delete()
diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search/base.py
similarity index 68%
rename from apiserver/plane/app/views/search.py
rename to apiserver/plane/app/views/search/base.py
index 15b05e83f..8a7b9d908 100644
--- a/apiserver/plane/app/views/search.py
+++ b/apiserver/plane/app/views/search/base.py
@@ -2,14 +2,17 @@
import re
# Django imports
-from django.db.models import Q
+from django.db.models import Q, OuterRef, Subquery, Value, UUIDField, CharField
+from django.contrib.postgres.aggregates import ArrayAgg
+from django.contrib.postgres.fields import ArrayField
+from django.db.models.functions import Coalesce
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
-from .base import BaseAPIView
+from plane.app.views.base import BaseAPIView
from plane.db.models import (
Workspace,
Project,
@@ -18,8 +21,8 @@ from plane.db.models import (
Module,
Page,
IssueView,
+ ProjectPage,
)
-from plane.utils.issue_search import search_issues
class GlobalSearchEndpoint(BaseAPIView):
@@ -145,22 +148,51 @@ class GlobalSearchEndpoint(BaseAPIView):
for field in fields:
q |= Q(**{f"{field}__icontains": query})
- pages = Page.objects.filter(
- q,
- projects__project_projectmember__member=self.request.user,
- projects__project_projectmember__is_active=True,
- projects__archived_at__isnull=True,
- workspace__slug=slug,
+ pages = (
+ Page.objects.filter(
+ q,
+ projects__project_projectmember__member=self.request.user,
+ projects__project_projectmember__is_active=True,
+ projects__archived_at__isnull=True,
+ workspace__slug=slug,
+ )
+ .annotate(
+ project_ids=Coalesce(
+ ArrayAgg(
+ "projects__id",
+ distinct=True,
+ filter=~Q(projects__id=True),
+ ),
+ Value([], output_field=ArrayField(UUIDField())),
+ ),
+ )
+ .annotate(
+ project_identifiers=Coalesce(
+ ArrayAgg(
+ "projects__identifier",
+ distinct=True,
+ filter=~Q(projects__id=True),
+ ),
+ Value([], output_field=ArrayField(CharField())),
+ ),
+ )
)
if workspace_search == "false" and project_id:
- pages = pages.filter(project_id=project_id)
+ project_subquery = ProjectPage.objects.filter(
+ page_id=OuterRef("id"),
+ project_id=project_id,
+ ).values_list("project_id", flat=True)[:1]
+
+ pages = pages.annotate(
+ project_id=Subquery(project_subquery)
+ ).filter(project_id=project_id)
return pages.distinct().values(
"name",
"id",
- "project_id",
- "project__identifier",
+ "project_ids",
+ "project_identifiers",
"workspace__slug",
)
@@ -228,76 +260,3 @@ class GlobalSearchEndpoint(BaseAPIView):
func = MODELS_MAPPER.get(model, None)
results[model] = func(query, slug, project_id, workspace_search)
return Response({"results": results}, status=status.HTTP_200_OK)
-
-
-class IssueSearchEndpoint(BaseAPIView):
- def get(self, request, slug, project_id):
- query = request.query_params.get("search", False)
- workspace_search = request.query_params.get(
- "workspace_search", "false"
- )
- parent = request.query_params.get("parent", "false")
- issue_relation = request.query_params.get("issue_relation", "false")
- cycle = request.query_params.get("cycle", "false")
- module = request.query_params.get("module", False)
- sub_issue = request.query_params.get("sub_issue", "false")
- target_date = request.query_params.get("target_date", True)
-
- issue_id = request.query_params.get("issue_id", False)
-
- issues = Issue.issue_objects.filter(
- workspace__slug=slug,
- project__project_projectmember__member=self.request.user,
- project__project_projectmember__is_active=True,
- project__archived_at__isnull=True,
- )
-
- if workspace_search == "false":
- issues = issues.filter(project_id=project_id)
-
- if query:
- issues = search_issues(query, issues)
-
- if parent == "true" and issue_id:
- issue = Issue.issue_objects.get(pk=issue_id)
- issues = issues.filter(
- ~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id)
- )
- if issue_relation == "true" and issue_id:
- issue = Issue.issue_objects.get(pk=issue_id)
- issues = issues.filter(
- ~Q(pk=issue_id),
- ~Q(issue_related__issue=issue),
- ~Q(issue_relation__related_issue=issue),
- )
- if sub_issue == "true" and issue_id:
- issue = Issue.issue_objects.get(pk=issue_id)
- issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
- if issue.parent:
- issues = issues.filter(~Q(pk=issue.parent_id))
-
- if cycle == "true":
- issues = issues.exclude(issue_cycle__isnull=False)
-
- if module:
- issues = issues.exclude(issue_module__module=module)
-
- if target_date == "none":
- issues = issues.filter(target_date__isnull=True)
-
- return Response(
- issues.values(
- "name",
- "id",
- "start_date",
- "sequence_id",
- "project__name",
- "project__identifier",
- "project_id",
- "workspace__slug",
- "state__name",
- "state__group",
- "state__color",
- ),
- status=status.HTTP_200_OK,
- )
diff --git a/apiserver/plane/app/views/search/issue.py b/apiserver/plane/app/views/search/issue.py
new file mode 100644
index 000000000..50b468715
--- /dev/null
+++ b/apiserver/plane/app/views/search/issue.py
@@ -0,0 +1,95 @@
+# Python imports
+import re
+
+# Django imports
+from django.db.models import Q
+
+# Third party imports
+from rest_framework import status
+from rest_framework.response import Response
+
+# Module imports
+from .base import BaseAPIView
+from plane.db.models import (
+ Workspace,
+ Project,
+ Issue,
+ Cycle,
+ Module,
+ Page,
+ IssueView,
+)
+from plane.utils.issue_search import search_issues
+
+
+class IssueSearchEndpoint(BaseAPIView):
+ def get(self, request, slug, project_id):
+ query = request.query_params.get("search", False)
+ workspace_search = request.query_params.get(
+ "workspace_search", "false"
+ )
+ parent = request.query_params.get("parent", "false")
+ issue_relation = request.query_params.get("issue_relation", "false")
+ cycle = request.query_params.get("cycle", "false")
+ module = request.query_params.get("module", False)
+ sub_issue = request.query_params.get("sub_issue", "false")
+ target_date = request.query_params.get("target_date", True)
+
+ issue_id = request.query_params.get("issue_id", False)
+
+ issues = Issue.issue_objects.filter(
+ workspace__slug=slug,
+ project__project_projectmember__member=self.request.user,
+ project__project_projectmember__is_active=True,
+ project__archived_at__isnull=True,
+ )
+
+ if workspace_search == "false":
+ issues = issues.filter(project_id=project_id)
+
+ if query:
+ issues = search_issues(query, issues)
+
+ if parent == "true" and issue_id:
+ issue = Issue.issue_objects.get(pk=issue_id)
+ issues = issues.filter(
+ ~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id)
+ )
+ if issue_relation == "true" and issue_id:
+ issue = Issue.issue_objects.get(pk=issue_id)
+ issues = issues.filter(
+ ~Q(pk=issue_id),
+ ~Q(issue_related__issue=issue),
+ ~Q(issue_relation__related_issue=issue),
+ )
+ if sub_issue == "true" and issue_id:
+ issue = Issue.issue_objects.get(pk=issue_id)
+ issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
+ if issue.parent:
+ issues = issues.filter(~Q(pk=issue.parent_id))
+
+ if cycle == "true":
+ issues = issues.exclude(issue_cycle__isnull=False)
+
+ if module:
+ issues = issues.exclude(issue_module__module=module)
+
+ if target_date == "none":
+ issues = issues.filter(target_date__isnull=True)
+
+ return Response(
+ issues.values(
+ "name",
+ "id",
+ "start_date",
+ "sequence_id",
+ "project__name",
+ "project__identifier",
+ "project_id",
+ "workspace__slug",
+ "state__name",
+ "state__group",
+ "state__color",
+ ),
+ status=status.HTTP_200_OK,
+ )
diff --git a/packages/types/src/pages.d.ts b/packages/types/src/pages.d.ts
index 1c94dfc06..9119c4e51 100644
--- a/packages/types/src/pages.d.ts
+++ b/packages/types/src/pages.d.ts
@@ -11,10 +11,10 @@ export type TPage = {
id: string | undefined;
is_favorite: boolean;
is_locked: boolean;
- labels: string[] | undefined;
+ label_ids: string[] | undefined;
name: string | undefined;
owned_by: string | undefined;
- project: string | undefined;
+ project_ids: string[] | undefined;
updated_at: Date | undefined;
updated_by: string | undefined;
workspace: string | undefined;
diff --git a/packages/types/src/workspace.d.ts b/packages/types/src/workspace.d.ts
index ceaa53d02..4e40009e1 100644
--- a/packages/types/src/workspace.d.ts
+++ b/packages/types/src/workspace.d.ts
@@ -112,6 +112,14 @@ export interface IWorkspaceIssueSearchResult {
workspace__slug: string;
}
+export interface IWorkspacePageSearchResult {
+ id: string;
+ name: string;
+ project_ids: string[];
+ project__identifiers: string[];
+ workspace__slug: string;
+}
+
export interface IWorkspaceProjectSearchResult {
id: string;
identifier: string;
@@ -127,7 +135,7 @@ export interface IWorkspaceSearchResults {
cycle: IWorkspaceDefaultSearchResult[];
module: IWorkspaceDefaultSearchResult[];
issue_view: IWorkspaceDefaultSearchResult[];
- page: IWorkspaceDefaultSearchResult[];
+ page: IWorkspacePageSearchResult[];
};
}
diff --git a/web/core/components/command-palette/helpers.tsx b/web/core/components/command-palette/helpers.tsx
index 0d986b944..c29e854fa 100644
--- a/web/core/components/command-palette/helpers.tsx
+++ b/web/core/components/command-palette/helpers.tsx
@@ -5,6 +5,7 @@ import { Briefcase, FileText, LayoutGrid } from "lucide-react";
import {
IWorkspaceDefaultSearchResult,
IWorkspaceIssueSearchResult,
+ IWorkspacePageSearchResult,
IWorkspaceProjectSearchResult,
IWorkspaceSearchResult,
} from "@plane/types";
@@ -67,9 +68,9 @@ export const commandGroups: {
},
page: {
icon: