diff --git a/apiserver/plane/app/urls/search.py b/apiserver/plane/app/urls/search.py index de4f1e7b2c0..0bbbd9cf7f4 100644 --- a/apiserver/plane/app/urls/search.py +++ b/apiserver/plane/app/urls/search.py @@ -16,7 +16,7 @@ name="project-issue-search", ), path( - "workspaces//projects//entity-search/", + "workspaces//entity-search/", SearchEndpoint.as_view(), name="entity-search", ), diff --git a/apiserver/plane/app/views/search/base.py b/apiserver/plane/app/views/search/base.py index 3736d8f81af..1f6754a9e7c 100644 --- a/apiserver/plane/app/views/search/base.py +++ b/apiserver/plane/app/views/search/base.py @@ -34,6 +34,7 @@ IssueView, ProjectMember, ProjectPage, + WorkspaceMember, ) @@ -252,214 +253,456 @@ def get(self, request, slug): class SearchEndpoint(BaseAPIView): - def get(self, request, slug, project_id): + def get(self, request, slug): query = request.query_params.get("query", False) query_types = request.query_params.get("query_type", "user_mention").split(",") query_types = [qt.strip() for qt in query_types] count = int(request.query_params.get("count", 5)) + project_id = request.query_params.get("project_id", None) + issue_id = request.query_params.get("issue_id", None) response_data = {} - for query_type in query_types: - if query_type == "user_mention": - fields = [ - "member__first_name", - "member__last_name", - "member__display_name", - ] - q = Q() - - if query: - for field in fields: - q |= Q(**{f"{field}__icontains": query}) - users = ( - ProjectMember.objects.filter( - q, is_active=True, project_id=project_id, workspace__slug=slug, member__is_bot=False + if project_id: + for query_type in query_types: + if query_type == "user_mention": + fields = [ + "member__first_name", + "member__last_name", + "member__display_name", + ] + q = Q() + + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + + base_filters = Q( + q, + is_active=True, + workspace__slug=slug, + member__is_bot=False, + project_id=project_id, + role__gt=10, ) - .annotate( - member__avatar_url=Case( - When( - member__avatar_asset__isnull=False, - then=Concat( - Value("/api/assets/v2/static/"), - "member__avatar_asset", - Value("/"), + if issue_id: + issue_created_by = ( + Issue.objects.filter(id=issue_id) + .values_list("created_by_id", flat=True) + .first() + ) + # Add condition to include `issue_created_by` in the query + filters = Q(member_id=issue_created_by) | base_filters + else: + filters = base_filters + + # Query to fetch users + users = ( + ProjectMember.objects.filter(filters) + .annotate( + member__avatar_url=Case( + When( + member__avatar_asset__isnull=False, + then=Concat( + Value("/api/assets/v2/static/"), + "member__avatar_asset", + Value("/"), + ), + ), + When( + member__avatar_asset__isnull=True, + then="member__avatar", ), - ), - When( - member__avatar_asset__isnull=True, then="member__avatar" - ), - default=Value(None), - output_field=models.CharField(), + default=Value(None), + output_field=CharField(), + ) ) + .order_by("-created_at") + .values( + "member__avatar_url", + "member__display_name", + "member__id", + )[:count] ) - .order_by("-created_at") - .values("member__avatar_url", "member__display_name", "member__id")[ - :count - ] - ) - response_data["user_mention"] = list(users) - elif query_type == "project": - fields = ["name", "identifier"] - q = Q() + response_data["user_mention"] = list(users) - if query: - for field in fields: - q |= Q(**{f"{field}__icontains": query}) - projects = ( - Project.objects.filter( - q, - Q(project_projectmember__member=self.request.user) - | Q(network=2), - workspace__slug=slug, + elif query_type == "project": + fields = ["name", "identifier"] + q = Q() + + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + projects = ( + Project.objects.filter( + q, + Q(project_projectmember__member=self.request.user) + | Q(network=2), + workspace__slug=slug, + ) + .order_by("-created_at") + .distinct() + .values( + "name", "id", "identifier", "logo_props", "workspace__slug" + )[:count] ) - .order_by("-created_at") - .distinct() - .values( - "name", "id", "identifier", "logo_props", "workspace__slug" - )[:count] - ) - response_data["project"] = list(projects) - - elif query_type == "issue": - fields = ["name", "sequence_id", "project__identifier"] - q = Q() - - if query: - for field in fields: - if field == "sequence_id": - sequences = re.findall(r"\b\d+\b", query) - for sequence_id in sequences: - q |= Q(**{"sequence_id": sequence_id}) - else: + response_data["project"] = list(projects) + + elif query_type == "issue": + fields = ["name", "sequence_id", "project__identifier"] + q = Q() + + if query: + for field in fields: + if field == "sequence_id": + sequences = re.findall(r"\b\d+\b", query) + for sequence_id in sequences: + q |= Q(**{"sequence_id": sequence_id}) + else: + q |= Q(**{f"{field}__icontains": query}) + + issues = ( + Issue.issue_objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + project_id=project_id, + ) + .order_by("-created_at") + .distinct() + .values( + "name", + "id", + "sequence_id", + "project__identifier", + "project_id", + "priority", + "state_id", + "type_id", + )[:count] + ) + response_data["issue"] = list(issues) + + elif query_type == "cycle": + fields = ["name"] + q = Q() + + if query: + for field in fields: q |= Q(**{f"{field}__icontains": query}) - issues = ( - Issue.issue_objects.filter( - q, - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, - project_id=project_id, + cycles = ( + Cycle.objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + project_id=project_id, + ) + .annotate( + status=Case( + When( + Q(start_date__lte=timezone.now()) + & Q(end_date__gte=timezone.now()), + then=Value("CURRENT"), + ), + When( + start_date__gt=timezone.now(), + then=Value("UPCOMING"), + ), + When( + end_date__lt=timezone.now(), then=Value("COMPLETED") + ), + When( + Q(start_date__isnull=True) + & Q(end_date__isnull=True), + then=Value("DRAFT"), + ), + default=Value("DRAFT"), + output_field=CharField(), + ) + ) + .order_by("-created_at") + .distinct() + .values( + "name", + "id", + "project_id", + "project__identifier", + "status", + "workspace__slug", + )[:count] ) - .order_by("-created_at") - .distinct() - .values( - "name", - "id", - "sequence_id", - "project__identifier", - "project_id", - "priority", - "state_id", - "type_id", - )[:count] - ) - response_data["issue"] = list(issues) + response_data["cycle"] = list(cycles) - elif query_type == "cycle": - fields = ["name"] - q = Q() + elif query_type == "module": + fields = ["name"] + q = Q() - if query: - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) - cycles = ( - Cycle.objects.filter( - q, - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, + modules = ( + Module.objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + project_id=project_id, + ) + .order_by("-created_at") + .distinct() + .values( + "name", + "id", + "project_id", + "project__identifier", + "status", + "workspace__slug", + )[:count] ) - .annotate( - status=Case( - When( - Q(start_date__lte=timezone.now()) - & Q(end_date__gte=timezone.now()), - then=Value("CURRENT"), - ), - When(start_date__gt=timezone.now(), then=Value("UPCOMING")), - When(end_date__lt=timezone.now(), then=Value("COMPLETED")), - When( - Q(start_date__isnull=True) & Q(end_date__isnull=True), - then=Value("DRAFT"), - ), - default=Value("DRAFT"), - output_field=CharField(), + response_data["module"] = list(modules) + + elif query_type == "page": + fields = ["name"] + q = Q() + + if query: + 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__id=project_id, + workspace__slug=slug, + access=0, ) + .order_by("-created_at") + .distinct() + .values( + "name", + "id", + "logo_props", + "projects__id", + "workspace__slug", + )[:count] ) - .order_by("-created_at") - .distinct() - .values( - "name", - "id", - "project_id", - "project__identifier", - "status", - "workspace__slug", - )[:count] - ) - response_data["cycle"] = list(cycles) + response_data["page"] = list(pages) + return Response(response_data, status=status.HTTP_200_OK) + + else: + for query_type in query_types: + if query_type == "user_mention": + fields = [ + "member__first_name", + "member__last_name", + "member__display_name", + ] + q = Q() - elif query_type == "module": - fields = ["name"] - q = Q() + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + users = ( + WorkspaceMember.objects.filter( + q, + is_active=True, + workspace__slug=slug, + member__is_bot=False, + ) + .annotate( + member__avatar_url=Case( + When( + member__avatar_asset__isnull=False, + then=Concat( + Value("/api/assets/v2/static/"), + "member__avatar_asset", + Value("/"), + ), + ), + When( + member__avatar_asset__isnull=True, + then="member__avatar", + ), + default=Value(None), + output_field=models.CharField(), + ) + ) + .order_by("-created_at") + .values( + "member__avatar_url", "member__display_name", "member__id" + )[:count] + ) + response_data["user_mention"] = list(users) - if query: - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + elif query_type == "project": + fields = ["name", "identifier"] + q = Q() - modules = ( - Module.objects.filter( - q, - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + projects = ( + Project.objects.filter( + q, + Q(project_projectmember__member=self.request.user) + | Q(network=2), + workspace__slug=slug, + ) + .order_by("-created_at") + .distinct() + .values( + "name", "id", "identifier", "logo_props", "workspace__slug" + )[:count] ) - .order_by("-created_at") - .distinct() - .values( - "name", - "id", - "project_id", - "project__identifier", - "status", - "workspace__slug", - )[:count] - ) - response_data["module"] = list(modules) + response_data["project"] = list(projects) + + elif query_type == "issue": + fields = ["name", "sequence_id", "project__identifier"] + q = Q() + + if query: + for field in fields: + if field == "sequence_id": + sequences = re.findall(r"\b\d+\b", query) + for sequence_id in sequences: + q |= Q(**{"sequence_id": sequence_id}) + else: + q |= Q(**{f"{field}__icontains": query}) + + issues = ( + Issue.issue_objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .order_by("-created_at") + .distinct() + .values( + "name", + "id", + "sequence_id", + "project__identifier", + "project_id", + "priority", + "state_id", + "type_id", + )[:count] + ) + response_data["issue"] = list(issues) - elif query_type == "page": - fields = ["name"] - q = Q() + elif query_type == "cycle": + fields = ["name"] + q = Q() - if query: - for field in fields: - q |= Q(**{f"{field}__icontains": query}) + if query: + 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__id=project_id, - workspace__slug=slug, - access=0, + cycles = ( + Cycle.objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .annotate( + status=Case( + When( + Q(start_date__lte=timezone.now()) + & Q(end_date__gte=timezone.now()), + then=Value("CURRENT"), + ), + When( + start_date__gt=timezone.now(), + then=Value("UPCOMING"), + ), + When( + end_date__lt=timezone.now(), then=Value("COMPLETED") + ), + When( + Q(start_date__isnull=True) + & Q(end_date__isnull=True), + then=Value("DRAFT"), + ), + default=Value("DRAFT"), + output_field=CharField(), + ) + ) + .order_by("-created_at") + .distinct() + .values( + "name", + "id", + "project_id", + "project__identifier", + "status", + "workspace__slug", + )[:count] ) - .order_by("-created_at") - .distinct() - .values( - "name", "id", "logo_props", "projects__id", "workspace__slug" - )[:count] - ) - response_data["page"] = list(pages) + response_data["cycle"] = list(cycles) - else: - return Response( - {"error": f"Invalid query type: {query_type}"}, - status=status.HTTP_400_BAD_REQUEST, - ) + elif query_type == "module": + fields = ["name"] + q = Q() - return Response(response_data, status=status.HTTP_200_OK) + if query: + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + + modules = ( + Module.objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .order_by("-created_at") + .distinct() + .values( + "name", + "id", + "project_id", + "project__identifier", + "status", + "workspace__slug", + )[:count] + ) + response_data["module"] = list(modules) + + elif query_type == "page": + fields = ["name"] + q = Q() + + if query: + 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, + workspace__slug=slug, + access=0, + is_global=True, + ) + .order_by("-created_at") + .distinct() + .values( + "name", + "id", + "logo_props", + "projects__id", + "workspace__slug", + )[:count] + ) + response_data["page"] = list(pages) + return Response(response_data, status=status.HTTP_200_OK) diff --git a/packages/types/src/search.d.ts b/packages/types/src/search.d.ts index 5ab01549c92..6eb1475129c 100644 --- a/packages/types/src/search.d.ts +++ b/packages/types/src/search.d.ts @@ -76,6 +76,8 @@ export type TSearchResponse = { export type TSearchEntityRequestPayload = { count: number; + project_id?: string; query_type: TSearchEntities[]; query: string; + team_id?: string; }; diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index d1a7b06405d..da3ac44923c 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -14,9 +14,9 @@ import { useEditorMention } from "@/hooks/use-editor-mention"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useFileSize } from "@/plane-web/hooks/use-file-size"; -// services -import { ProjectService } from "@/services/project"; -const projectService = new ProjectService(); +// plane web services +import { WorkspaceService } from "@/plane-web/services"; +const workspaceService = new WorkspaceService(); interface LiteTextEditorWrapperProps extends Omit { @@ -55,7 +55,10 @@ export const LiteTextEditor = React.forwardRef - await projectService.searchEntity(workspaceSlug?.toString() ?? "", projectId?.toString() ?? "", payload), + await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", { + ...payload, + project_id: projectId?.toString() ?? "", + }), }); // file size const { maxFileSize } = useFileSize(); diff --git a/web/core/components/inbox/modals/create-modal/issue-description.tsx b/web/core/components/inbox/modals/create-modal/issue-description.tsx index 14816cbc0d2..7f279e92a99 100644 --- a/web/core/components/inbox/modals/create-modal/issue-description.tsx +++ b/web/core/components/inbox/modals/create-modal/issue-description.tsx @@ -19,11 +19,12 @@ import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useProjectInbox } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane web services +import { WorkspaceService } from "@/plane-web/services"; // services import { FileService } from "@/services/file.service"; -import { ProjectService } from "@/services/project"; const fileService = new FileService(); -const projectService = new ProjectService(); +const workspaceService = new WorkspaceService(); type TInboxIssueDescription = { containerClassName?: string; @@ -75,7 +76,10 @@ export const InboxIssueDescription: FC = observer((props onChange={(_description: object, description_html: string) => handleData("description_html", description_html)} placeholder={getDescriptionPlaceholder} searchMentionCallback={async (payload) => - await projectService.searchEntity(workspaceSlug?.toString() ?? "", projectId?.toString() ?? "", payload) + await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", { + ...payload, + project_id: projectId?.toString() ?? "", + }) } containerClassName={containerClassName} onEnterKeyPress={onEnterKeyPress} diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx index 32964cec1f6..9008c9f7687 100644 --- a/web/core/components/issues/description-input.tsx +++ b/web/core/components/issues/description-input.tsx @@ -16,11 +16,12 @@ import { TIssueOperations } from "@/components/issues/issue-detail"; import { getDescriptionPlaceholder } from "@/helpers/issue.helper"; // hooks import { useWorkspace } from "@/hooks/store"; +// plane web services +import { WorkspaceService } from "@/plane-web/services"; // services import { FileService } from "@/services/file.service"; -import { ProjectService } from "@/services/project"; +const workspaceService = new WorkspaceService(); const fileService = new FileService(); -const projectService = new ProjectService(); export type IssueDescriptionInputProps = { containerClassName?: string; @@ -121,11 +122,10 @@ export const IssueDescriptionInput: FC = observer((p placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value) } searchMentionCallback={async (payload) => - await projectService.searchEntity( - workspaceSlug?.toString() ?? "", - projectId?.toString() ?? "", - payload - ) + await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", { + ...payload, + project_id: projectId?.toString() ?? "", + }) } containerClassName={containerClassName} uploadFile={async (file) => { diff --git a/web/core/components/issues/issue-modal/components/description-editor.tsx b/web/core/components/issues/issue-modal/components/description-editor.tsx index 19ddbc1f40a..8fc7b115b62 100644 --- a/web/core/components/issues/issue-modal/components/description-editor.tsx +++ b/web/core/components/issues/issue-modal/components/description-editor.tsx @@ -23,10 +23,11 @@ import { getTabIndex } from "@/helpers/tab-indices.helper"; import { useInstance, useWorkspace } from "@/hooks/store"; import useKeypress from "@/hooks/use-keypress"; import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane web services +import { WorkspaceService } from "@/plane-web/services"; // services import { AIService } from "@/services/ai.service"; import { FileService } from "@/services/file.service"; -import { ProjectService } from "@/services/project"; type TIssueDescriptionEditorProps = { control: Control; @@ -48,9 +49,9 @@ type TIssueDescriptionEditorProps = { }; // services +const workspaceService = new WorkspaceService(); const aiService = new AIService(); const fileService = new FileService(); -const projectService = new ProjectService(); export const IssueDescriptionEditor: React.FC = observer((props) => { const { @@ -191,11 +192,10 @@ export const IssueDescriptionEditor: React.FC = ob tabIndex={getIndex("description_html")} placeholder={getDescriptionPlaceholder} searchMentionCallback={async (payload) => - await projectService.searchEntity( - workspaceSlug?.toString() ?? "", - projectId?.toString() ?? "", - payload - ) + await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", { + ...payload, + project_id: projectId?.toString() ?? "", + }) } containerClassName="pt-3 min-h-[120px]" uploadFile={async (file) => { diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index ddd45f62286..2f1bce38598 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -30,14 +30,15 @@ import { EditorAIMenu } from "@/plane-web/components/pages"; import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useFileSize } from "@/plane-web/hooks/use-file-size"; import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; +// plane web services +import { WorkspaceService } from "@/plane-web/services"; // services import { FileService } from "@/services/file.service"; -import { ProjectService } from "@/services/project"; // store import { IPage } from "@/store/pages/page"; // services init +const workspaceService = new WorkspaceService(); const fileService = new FileService(); -const projectService = new ProjectService(); type Props = { editorRef: React.RefObject; @@ -63,7 +64,10 @@ export const PageEditorBody: React.FC = observer((props) => { // use editor mention const { fetchMentions } = useEditorMention({ searchEntity: async (payload) => - await projectService.searchEntity(workspaceSlug?.toString() ?? "", projectId?.toString() ?? "", payload), + await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", { + ...payload, + project_id: projectId?.toString() ?? "", + }), }); // editor flaggings const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); diff --git a/web/core/services/project/project.service.ts b/web/core/services/project/project.service.ts index e3be9c5f251..f9c2af8b66a 100644 --- a/web/core/services/project/project.service.ts +++ b/web/core/services/project/project.service.ts @@ -1,10 +1,4 @@ -import type { - GithubRepositoriesResponse, - ISearchIssueResponse, - TProjectIssuesSearchParams, - TSearchEntityRequestPayload, - TSearchResponse, -} from "@plane/types"; +import type { GithubRepositoriesResponse, ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; // plane web types @@ -170,21 +164,4 @@ export class ProjectService extends APIService { throw error?.response?.data; }); } - - async searchEntity( - workspaceSlug: string, - projectId: string, - params: TSearchEntityRequestPayload - ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/entity-search/`, { - params: { - ...params, - query_type: params.query_type.join(","), - }, - }) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } } diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index e1aa9b7cd2b..6fc0d21b429 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -12,6 +12,8 @@ import { IUserProjectsRole, IWorkspaceView, TIssuesResponse, + TSearchResponse, + TSearchEntityRequestPayload, } from "@plane/types"; import { APIService } from "@/services/api.service"; // helpers @@ -277,4 +279,17 @@ export class WorkspaceService extends APIService { throw error?.response?.data; }); } + + async searchEntity(workspaceSlug: string, params: TSearchEntityRequestPayload): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/entity-search/`, { + params: { + ...params, + query_type: params.query_type.join(","), + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } }