mirror of
https://github.com/makeplane/plane.git
synced 2025-12-29 00:24:56 +01:00
Merge pull request #415 from makeplane/sync/ce-ee
sync: community changes
This commit is contained in:
@@ -169,7 +169,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
|
||||
@@ -191,18 +199,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"]
|
||||
@@ -211,6 +215,8 @@ 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,
|
||||
@@ -218,6 +224,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,
|
||||
@@ -226,6 +233,7 @@ class PageSerializer(BaseSerializer):
|
||||
updated_by_id=page.updated_by_id,
|
||||
)
|
||||
|
||||
# Create page labels
|
||||
if labels is not None:
|
||||
PageLabel.objects.bulk_create(
|
||||
[
|
||||
|
||||
@@ -186,6 +186,7 @@ from .search.base import (
|
||||
SearchEndpoint,
|
||||
)
|
||||
|
||||
from .search.base import GlobalSearchEndpoint
|
||||
from .search.issue import IssueSearchEndpoint
|
||||
from .search.workspace import (
|
||||
WorkspaceSearchEndpoint,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,7 +21,7 @@ from plane.db.models import (
|
||||
Module,
|
||||
Page,
|
||||
IssueView,
|
||||
ProjectMember,
|
||||
ProjectPage,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,201 +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 SearchEndpoint(BaseAPIView):
|
||||
def get(self, request, slug, project_id):
|
||||
query = request.query_params.get("query", False)
|
||||
query_type = request.query_params.get("query_type", "issue")
|
||||
count = int(request.query_params.get("count", 5))
|
||||
|
||||
if query_type == "mention":
|
||||
fields = ["member__first_name", "member__last_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,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.values(
|
||||
"member__first_name",
|
||||
"member__last_name",
|
||||
"member__avatar",
|
||||
"member__display_name",
|
||||
"member__id",
|
||||
)[:count]
|
||||
)
|
||||
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
|
||||
if query:
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
pages = (
|
||||
Page.objects.filter(
|
||||
q,
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
workspace__slug=slug,
|
||||
access=0,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.values("name", "id")[:count]
|
||||
)
|
||||
return Response(
|
||||
{"users": users, "pages": pages}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
if 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", "workspace__slug")[:count]
|
||||
)
|
||||
return Response(projects, status=status.HTTP_200_OK)
|
||||
|
||||
if query_type == "issue":
|
||||
fields = ["name", "sequence_id", "project__identifier"]
|
||||
q = Q()
|
||||
|
||||
if query:
|
||||
for field in fields:
|
||||
if field == "sequence_id":
|
||||
# Match whole integers only (exclude decimal numbers)
|
||||
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",
|
||||
)[:count]
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
if 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,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name",
|
||||
"id",
|
||||
"project_id",
|
||||
"project__identifier",
|
||||
"workspace__slug",
|
||||
)[:count]
|
||||
)
|
||||
return Response(cycles, status=status.HTTP_200_OK)
|
||||
|
||||
if 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",
|
||||
"workspace__slug",
|
||||
)[:count]
|
||||
)
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
|
||||
if query_type == "page":
|
||||
fields = ["name"]
|
||||
q = Q()
|
||||
|
||||
if query:
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
pages = (
|
||||
Page.objects.filter(
|
||||
q,
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
access=0,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name",
|
||||
"id",
|
||||
"project_id",
|
||||
"project__identifier",
|
||||
"workspace__slug",
|
||||
)[:count]
|
||||
)
|
||||
return Response(pages, status=status.HTTP_200_OK)
|
||||
|
||||
return Response(
|
||||
{"error": "Please provide a valid query"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from ..base import BaseAPIView
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
)
|
||||
|
||||
@@ -147,7 +147,7 @@ services:
|
||||
|
||||
plane-redis:
|
||||
<<: *app-env
|
||||
image: redis:7.2.4-alpine
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -9,7 +9,7 @@ volumes:
|
||||
|
||||
services:
|
||||
plane-redis:
|
||||
image: redis:7.2.4-alpine
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
|
||||
@@ -125,7 +125,7 @@ services:
|
||||
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:7.2.4-alpine
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
4
packages/types/src/pages.d.ts
vendored
4
packages/types/src/pages.d.ts
vendored
@@ -12,10 +12,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;
|
||||
|
||||
10
packages/types/src/workspace.d.ts
vendored
10
packages/types/src/workspace.d.ts
vendored
@@ -113,6 +113,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;
|
||||
@@ -128,7 +136,7 @@ export interface IWorkspaceSearchResults {
|
||||
cycle: IWorkspaceDefaultSearchResult[];
|
||||
module: IWorkspaceDefaultSearchResult[];
|
||||
issue_view: IWorkspaceDefaultSearchResult[];
|
||||
page: IWorkspaceDefaultSearchResult[];
|
||||
page: IWorkspacePageSearchResult[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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: <FileText className="h-3 w-3" />,
|
||||
itemName: (page: IWorkspaceDefaultSearchResult) => (
|
||||
itemName: (page: IWorkspacePageSearchResult) => (
|
||||
<h6>
|
||||
<span className="text-xs text-custom-text-300">{page.project__identifier}</span> {page.name}
|
||||
<span className="text-xs text-custom-text-300">{page.project__identifiers?.[0]}</span> {page.name}
|
||||
</h6>
|
||||
),
|
||||
path: (page: IWorkspaceDefaultSearchResult) =>
|
||||
|
||||
@@ -146,7 +146,6 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
|
||||
.finally(() => setIsRestoring(false));
|
||||
};
|
||||
|
||||
|
||||
// auth
|
||||
const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
const canRestoreIssue = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
Signal,
|
||||
Tag,
|
||||
Triangle,
|
||||
UserCircle2,
|
||||
Users,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
// components
|
||||
import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon } from "@plane/ui";
|
||||
import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, Tooltip } from "@plane/ui";
|
||||
import {
|
||||
DateDropdown,
|
||||
EstimateDropdown,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from "@/components/dropdowns";
|
||||
// ui
|
||||
// helpers
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import {
|
||||
IssueCycleSelect,
|
||||
IssueLabel,
|
||||
@@ -40,7 +42,8 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
// types
|
||||
import { useProjectEstimates, useIssueDetail, useProject, useProjectState } from "@/hooks/store";
|
||||
import { useProjectEstimates, useIssueDetail, useProject, useProjectState, useMember } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// components
|
||||
import type { TIssueOperations } from "./root";
|
||||
// icons
|
||||
@@ -63,10 +66,14 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getUserDetails } = useMember();
|
||||
const { getStateById } = useProjectState();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
|
||||
const createdByDetails = getUserDetails(issue.created_by);
|
||||
|
||||
// derived values
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
@@ -143,6 +150,21 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{createdByDetails && (
|
||||
<div className="flex h-8 items-center gap-2">
|
||||
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Created by</span>
|
||||
</div>
|
||||
<Tooltip tooltipContent={createdByDetails?.display_name} isMobile={isMobile}>
|
||||
<div className="h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-default">
|
||||
<ButtonAvatars showTooltip={false} userIds={createdByDetails.id} />
|
||||
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex h-8 items-center gap-2">
|
||||
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
||||
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
CalendarClock,
|
||||
CalendarCheck2,
|
||||
Users,
|
||||
UserCircle2,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
// ui icons
|
||||
import { DiceIcon, DoubleCircleIcon, ContrastIcon, RelatedIcon } from "@plane/ui";
|
||||
import { DiceIcon, DoubleCircleIcon, ContrastIcon, RelatedIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
DateDropdown,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
MemberDropdown,
|
||||
StateDropdown,
|
||||
} from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import {
|
||||
IssueLinkRoot,
|
||||
IssueCycleSelect,
|
||||
@@ -38,7 +40,8 @@ import {
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { useIssueDetail, useProject, useProjectState } from "@/hooks/store";
|
||||
import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
interface IPeekOverviewProperties {
|
||||
workspaceSlug: string;
|
||||
@@ -56,9 +59,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getStateById } = useProjectState();
|
||||
const { getUserDetails } = useMember();
|
||||
const { isMobile } = usePlatformOS();
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
const createdByDetails = getUserDetails(issue?.created_by);
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
const isEstimateEnabled = projectDetails?.estimate;
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
@@ -134,6 +140,22 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* created by */}
|
||||
{createdByDetails && (
|
||||
<div className="flex w-full items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Created by</span>
|
||||
</div>
|
||||
<Tooltip tooltipContent={createdByDetails?.display_name} isMobile={isMobile}>
|
||||
<div className="h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-default">
|
||||
<ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />
|
||||
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* start date */}
|
||||
<div className="flex w-full items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
|
||||
@@ -53,14 +53,14 @@ export class Page implements IPage {
|
||||
logo_props: TLogoProps | undefined;
|
||||
description_html: string | undefined;
|
||||
color: string | undefined;
|
||||
labels: string[] | undefined;
|
||||
label_ids: string[] | undefined;
|
||||
owned_by: string | undefined;
|
||||
access: EPageAccess | undefined;
|
||||
is_favorite: boolean;
|
||||
is_locked: boolean;
|
||||
archived_at: string | null | undefined;
|
||||
workspace: string | undefined;
|
||||
project: string | undefined;
|
||||
project_ids: string[] | undefined;
|
||||
created_by: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
created_at: Date | undefined;
|
||||
@@ -81,14 +81,14 @@ export class Page implements IPage {
|
||||
this.logo_props = page?.logo_props || undefined;
|
||||
this.description_html = page?.description_html || undefined;
|
||||
this.color = page?.color || undefined;
|
||||
this.labels = page?.labels || undefined;
|
||||
this.label_ids = page?.label_ids || undefined;
|
||||
this.owned_by = page?.owned_by || undefined;
|
||||
this.access = page?.access || EPageAccess.PUBLIC;
|
||||
this.is_favorite = page?.is_favorite || false;
|
||||
this.is_locked = page?.is_locked || false;
|
||||
this.archived_at = page?.archived_at || undefined;
|
||||
this.workspace = page?.workspace || undefined;
|
||||
this.project = page?.project || undefined;
|
||||
this.project_ids = page?.project_ids || undefined;
|
||||
this.created_by = page?.created_by || undefined;
|
||||
this.updated_by = page?.updated_by || undefined;
|
||||
this.created_at = page?.created_at || undefined;
|
||||
@@ -104,14 +104,14 @@ export class Page implements IPage {
|
||||
logo_props: observable.ref,
|
||||
description_html: observable.ref,
|
||||
color: observable.ref,
|
||||
labels: observable,
|
||||
label_ids: observable,
|
||||
owned_by: observable.ref,
|
||||
access: observable.ref,
|
||||
is_favorite: observable.ref,
|
||||
is_locked: observable.ref,
|
||||
archived_at: observable.ref,
|
||||
workspace: observable.ref,
|
||||
project: observable.ref,
|
||||
project_ids: observable,
|
||||
created_by: observable.ref,
|
||||
updated_by: observable.ref,
|
||||
created_at: observable.ref,
|
||||
@@ -181,7 +181,7 @@ export class Page implements IPage {
|
||||
name: this.name,
|
||||
description_html: this.description_html,
|
||||
color: this.color,
|
||||
labels: this.labels,
|
||||
label_ids: this.label_ids,
|
||||
owned_by: this.owned_by,
|
||||
access: this.access,
|
||||
logo_props: this.logo_props,
|
||||
@@ -189,7 +189,7 @@ export class Page implements IPage {
|
||||
is_locked: this.is_locked,
|
||||
archived_at: this.archived_at,
|
||||
workspace: this.workspace,
|
||||
project: this.project,
|
||||
project_ids: this.project_ids,
|
||||
created_by: this.created_by,
|
||||
updated_by: this.updated_by,
|
||||
created_at: this.created_at,
|
||||
|
||||
@@ -95,7 +95,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
if (!projectId) return undefined;
|
||||
// helps to filter pages based on the pageType
|
||||
let pagesByType = filterPagesByPageType(pageType, Object.values(this?.data || {}));
|
||||
pagesByType = pagesByType.filter((p) => p.project === projectId);
|
||||
pagesByType = pagesByType.filter((p) => p.project_ids?.includes(projectId));
|
||||
|
||||
const pages = (pagesByType.map((page) => page.id) as string[]) || undefined;
|
||||
|
||||
@@ -114,7 +114,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
const pagesByType = filterPagesByPageType(pageType, Object.values(this?.data || {}));
|
||||
let filteredPages = pagesByType.filter(
|
||||
(p) =>
|
||||
p.project === projectId &&
|
||||
p.project_ids?.includes(projectId) &&
|
||||
getPageName(p.name).toLowerCase().includes(this.filters.searchQuery.toLowerCase()) &&
|
||||
shouldFilterPage(p, this.filters.filters)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user