chore: workspace entity search endpoint (#6272)
* chore: workspace entity search endpoint * fix: editor entity search endpoint * chore: restrict guest users --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
2d9464e841
commit
f54f3a6091
10 changed files with 481 additions and 233 deletions
|
|
@ -16,7 +16,7 @@ urlpatterns = [
|
|||
name="project-issue-search",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/entity-search/",
|
||||
"workspaces/<str:slug>/entity-search/",
|
||||
SearchEndpoint.as_view(),
|
||||
name="entity-search",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from plane.db.models import (
|
|||
IssueView,
|
||||
ProjectMember,
|
||||
ProjectPage,
|
||||
WorkspaceMember,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -252,14 +253,17 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
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 = {}
|
||||
|
||||
if project_id:
|
||||
for query_type in query_types:
|
||||
if query_type == "user_mention":
|
||||
fields = [
|
||||
|
|
@ -272,10 +276,29 @@ class SearchEndpoint(BaseAPIView):
|
|||
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
|
||||
|
||||
base_filters = Q(
|
||||
q,
|
||||
is_active=True,
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
project_id=project_id,
|
||||
role__gt=10,
|
||||
)
|
||||
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(
|
||||
|
|
@ -287,17 +310,21 @@ class SearchEndpoint(BaseAPIView):
|
|||
),
|
||||
),
|
||||
When(
|
||||
member__avatar_asset__isnull=True, then="member__avatar"
|
||||
member__avatar_asset__isnull=True,
|
||||
then="member__avatar",
|
||||
),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.values("member__avatar_url", "member__display_name", "member__id")[
|
||||
:count
|
||||
]
|
||||
.values(
|
||||
"member__avatar_url",
|
||||
"member__display_name",
|
||||
"member__id",
|
||||
)[:count]
|
||||
)
|
||||
|
||||
response_data["user_mention"] = list(users)
|
||||
|
||||
elif query_type == "project":
|
||||
|
|
@ -372,6 +399,7 @@ class SearchEndpoint(BaseAPIView):
|
|||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(
|
||||
status=Case(
|
||||
|
|
@ -380,10 +408,228 @@ class SearchEndpoint(BaseAPIView):
|
|||
& 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),
|
||||
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]
|
||||
)
|
||||
response_data["cycle"] = list(cycles)
|
||||
|
||||
elif query_type == "module":
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
|
||||
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,
|
||||
project_id=project_id,
|
||||
)
|
||||
.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,
|
||||
projects__id=project_id,
|
||||
workspace__slug=slug,
|
||||
access=0,
|
||||
)
|
||||
.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)
|
||||
|
||||
else:
|
||||
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 = (
|
||||
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)
|
||||
|
||||
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]
|
||||
)
|
||||
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 == "cycle":
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
|
||||
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,
|
||||
)
|
||||
.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"),
|
||||
|
|
@ -444,22 +690,19 @@ class SearchEndpoint(BaseAPIView):
|
|||
q,
|
||||
projects__project_projectmember__member=self.request.user,
|
||||
projects__project_projectmember__is_active=True,
|
||||
projects__id=project_id,
|
||||
workspace__slug=slug,
|
||||
access=0,
|
||||
is_global=True,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name", "id", "logo_props", "projects__id", "workspace__slug"
|
||||
"name",
|
||||
"id",
|
||||
"logo_props",
|
||||
"projects__id",
|
||||
"workspace__slug",
|
||||
)[:count]
|
||||
)
|
||||
response_data["page"] = list(pages)
|
||||
|
||||
else:
|
||||
return Response(
|
||||
{"error": f"Invalid query type: {query_type}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
2
packages/types/src/search.d.ts
vendored
2
packages/types/src/search.d.ts
vendored
|
|
@ -76,6 +76,8 @@ export type TSearchResponse = {
|
|||
|
||||
export type TSearchEntityRequestPayload = {
|
||||
count: number;
|
||||
project_id?: string;
|
||||
query_type: TSearchEntities[];
|
||||
query: string;
|
||||
team_id?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||
|
|
@ -55,7 +55,10 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||
// 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() ?? "",
|
||||
}),
|
||||
});
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
|
|
|
|||
|
|
@ -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<TInboxIssueDescription> = 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}
|
||||
|
|
|
|||
|
|
@ -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<IssueDescriptionInputProps> = 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) => {
|
||||
|
|
|
|||
|
|
@ -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<TIssue>;
|
||||
|
|
@ -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<TIssueDescriptionEditorProps> = observer((props) => {
|
||||
const {
|
||||
|
|
@ -191,11 +192,10 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = 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) => {
|
||||
|
|
|
|||
|
|
@ -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<EditorRefApi>;
|
||||
|
|
@ -63,7 +64,10 @@ export const PageEditorBody: React.FC<Props> = 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());
|
||||
|
|
|
|||
|
|
@ -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<TSearchResponse> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TSearchResponse> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue