Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PE-93] refactor: editor mentions extension #6178

Open
wants to merge 15 commits into
base: preview
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apiserver/plane/app/urls/search.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.urls import path


from plane.app.views import GlobalSearchEndpoint, IssueSearchEndpoint
from plane.app.views import GlobalSearchEndpoint, IssueSearchEndpoint, SearchEndpoint


urlpatterns = [
Expand All @@ -15,4 +15,9 @@
IssueSearchEndpoint.as_view(),
name="project-issue-search",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/entity-search/",
SearchEndpoint.as_view(),
name="entity-search",
),
]
2 changes: 1 addition & 1 deletion apiserver/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
)
from .page.version import PageVersionEndpoint

from .search.base import GlobalSearchEndpoint
from .search.base import GlobalSearchEndpoint, SearchEndpoint
from .search.issue import IssueSearchEndpoint


Expand Down
234 changes: 232 additions & 2 deletions apiserver/plane/app/views/search/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@
import re

# Django imports
from django.db.models import Q, OuterRef, Subquery, Value, UUIDField, CharField
from django.db import models
from django.db.models import (
Q,
OuterRef,
Subquery,
Value,
UUIDField,
CharField,
When,
Case,
)
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models.functions import Coalesce
from django.db.models.functions import Coalesce, Concat
from django.utils import timezone

# Third party imports
from rest_framework import status
Expand All @@ -21,6 +32,7 @@
Module,
Page,
IssueView,
ProjectMember,
ProjectPage,
)

Expand Down Expand Up @@ -237,3 +249,221 @@ def get(self, request, slug):
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 SearchEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
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]
NarayanBavisetti marked this conversation as resolved.
Show resolved Hide resolved
count = int(request.query_params.get("count", 5))
NarayanBavisetti marked this conversation as resolved.
Show resolved Hide resolved

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,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project_id=project_id,
workspace__slug=slug,
)
.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,
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})

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]
)
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,
)
.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)

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)
1 change: 1 addition & 0 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"dependencies": {
"@floating-ui/react": "^0.26.4",
"@hocuspocus/provider": "^2.13.5",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@tiptap/core": "^2.1.13",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TExtensions, TFileHandler } from "@/types";
import { EditorReadOnlyRefApi, TDisplayConfig, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";

interface IDocumentReadOnlyEditor {
disabledExtensions: TExtensions[];
Expand All @@ -23,9 +23,7 @@ interface IDocumentReadOnlyEditor {
fileHandler: Pick<TFileHandler, "getAssetSrc">;
tabIndex?: number;
handleEditorReady?: (value: boolean) => void;
mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>;
};
mentionHandler: TReadOnlyMentionHandler;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Editor, Extension } from "@tiptap/core";
import { Editor, Extensions } from "@tiptap/core";
// components
import { EditorContainer } from "@/components/editors";
// constants
Expand All @@ -12,7 +12,7 @@ import { EditorContentWrapper } from "./editor-content";

type Props = IEditorProps & {
children?: (editor: Editor) => React.ReactNode;
extensions: Extension<any, any>[];
extensions: Extensions;
};

export const EditorWrapper: React.FC<Props> = (props) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/core/extensions/core-without-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { CustomHorizontalRule } from "./horizontal-rule";
import { ImageExtensionWithoutProps } from "./image";
import { CustomImageComponentWithoutProps } from "./image/image-component-without-props";
import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props";
import { CustomMentionWithoutProps } from "./mentions/mentions-without-props";
import { CustomMentionExtensionConfig } from "./mentions/extension-config";
import { CustomQuoteExtension } from "./quote";
import { TableHeader, TableCell, TableRow, Table } from "./table";
import { CustomTextAlignExtension } from "./text-align";
Expand Down Expand Up @@ -97,7 +97,7 @@ export const CoreEditorExtensionsWithoutProps = [
TableHeader,
TableCell,
TableRow,
CustomMentionWithoutProps(),
CustomMentionExtensionConfig,
CustomTextAlignExtension,
CustomCalloutExtensionConfig,
CustomColorExtension,
Expand Down
17 changes: 5 additions & 12 deletions packages/editor/src/core/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
CustomImageExtension,
CustomKeymap,
CustomLinkExtension,
CustomMention,
CustomMentionExtension,
CustomQuoteExtension,
CustomTextAlignExtension,
CustomTypographyExtension,
Expand All @@ -33,24 +33,21 @@ import {
// helpers
import { isValidHttpUrl } from "@/helpers/common";
// types
import { IMentionHighlight, IMentionSuggestion, TExtensions, TFileHandler } from "@/types";
import { TExtensions, TFileHandler, TMentionHandler } from "@/types";
// plane editor extensions
import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";

type TArguments = {
disabledExtensions: TExtensions[];
enableHistory: boolean;
fileHandler: TFileHandler;
mentionConfig: {
mentionSuggestions?: () => Promise<IMentionSuggestion[]>;
mentionHighlights?: () => Promise<IMentionHighlight[]>;
};
mentionHandler: TMentionHandler;
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
};

export const CoreEditorExtensions = (args: TArguments): Extensions => {
const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex } = args;

return [
StarterKit.configure({
Expand Down Expand Up @@ -144,11 +141,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
TableHeader,
TableCell,
TableRow,
CustomMention({
mentionSuggestions: mentionConfig.mentionSuggestions,
mentionHighlights: mentionConfig.mentionHighlights,
readonly: false,
}),
CustomMentionExtension(mentionHandler),
Placeholder.configure({
placeholder: ({ editor, node }) => {
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
Expand Down
Loading