Merge pull request #2252 from makeplane/sync/ce-ee

Sync: Community Changes
This commit is contained in:
pushya22
2025-01-21 20:40:17 +05:30
committed by GitHub
12 changed files with 234 additions and 19 deletions

View File

@@ -73,6 +73,8 @@ from .issue import (
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from .module import (

View File

@@ -33,6 +33,8 @@ from plane.db.models import (
IssueVote,
IssueRelation,
State,
IssueVersion,
IssueDescriptionVersion,
IssueType,
)
@@ -687,3 +689,64 @@ class IssueSubscriberSerializer(BaseSerializer):
model = IssueSubscriber
fields = "__all__"
read_only_fields = ["workspace", "project", "issue"]
class IssueVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueVersion
fields = [
"id",
"workspace",
"project",
"issue",
"parent",
"state",
"estimate_point",
"name",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
"is_draft",
"external_source",
"external_id",
"type",
"cycle",
"modules",
"meta",
"name",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
class IssueDescriptionVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"project",
"issue",
"description_binary",
"description_html",
"description_stripped",
"description_json",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]

View File

@@ -23,6 +23,8 @@ from plane.app.views import (
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
IssueDescriptionVersionEndpoint,
)
urlpatterns = [
@@ -250,4 +252,24 @@ urlpatterns = [
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
]

View File

@@ -141,6 +141,8 @@ from .issue.sub_issue import SubIssuesEndpoint
from .issue.subscriber import IssueSubscriberViewSet
from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint
from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,

View File

@@ -0,0 +1,118 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.db.models import IssueVersion, IssueDescriptionVersion
from ..base import BaseAPIView
from plane.app.serializers import (
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from plane.app.permissions import allow_permission, ROLE
from plane.utils.global_paginator import paginate
from plane.utils.timezone_converter import user_timezone_converter
class IssueVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)
return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_version = IssueVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
serializer = IssueVersionDetailSerializer(issue_version)
return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None)
required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
issue_versions_queryset = IssueVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_versions_queryset,
queryset=issue_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)
class IssueDescriptionVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)
return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_description_version = IssueDescriptionVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
serializer = IssueDescriptionVersionDetailSerializer(
issue_description_version
)
return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None)
required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_description_versions_queryset,
queryset=issue_description_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)

View File

@@ -8,7 +8,7 @@ export const STICKY_COLORS_LIST: {
{
key: "gray",
label: "Gray",
backgroundColor: "var(--editor-colors-gray-background)",
backgroundColor: "rgba(var(--color-background-90))",
},
{
key: "peach",

View File

@@ -43,7 +43,6 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
showToolbarInitially = true,
showToolbar = true,
parentClassName = "",
placeholder = "Add comment...",
uploadFile,
...rest
} = props;
@@ -78,7 +77,6 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
mentionHandler={{
renderComponent: () => <></>,
}}
placeholder={placeholder}
containerClassName={cn(containerClassName, "relative")}
{...rest}
/>

View File

@@ -57,9 +57,11 @@ export const IssueCommentBlock: FC<TIssueCommentBlock> = observer((props) => {
>
{comment.actor_detail.avatar_url && comment.actor_detail.avatar_url !== "" ? (
<img
src={getFileURL(comment.actor_detail.avatar_url)}
src={getFileURL(comment.actor_detail?.avatar_url)}
alt={
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
comment.actor_detail?.is_bot
? comment.actor_detail?.first_name + " Bot"
: comment.actor_detail?.display_name
}
height={30}
width={30}
@@ -67,9 +69,9 @@ export const IssueCommentBlock: FC<TIssueCommentBlock> = observer((props) => {
/>
) : (
<>
{comment.actor_detail.is_bot
? comment.actor_detail.first_name.charAt(0)
: comment.actor_detail.display_name.charAt(0)}
{comment.actor_detail?.is_bot
? comment.actor_detail?.first_name.charAt(0)
: comment.actor_detail?.display_name.charAt(0)}
</>
)}
<div
@@ -93,9 +95,9 @@ export const IssueCommentBlock: FC<TIssueCommentBlock> = observer((props) => {
<div className="w-full truncate space-y-1">
<div>
<div className="text-xs capitalize">
{comment.actor_detail.is_bot
? comment.actor_detail.first_name + " Bot"
: comment.actor_detail.display_name}
{comment.actor_detail?.is_bot
? comment.actor_detail?.first_name + " Bot"
: comment.actor_detail?.display_name}
</div>
<div className="text-xs text-custom-text-200">commented {calculateTimeAgo(comment.created_at)}</div>
</div>

View File

@@ -4,13 +4,15 @@ import { Controller, useForm } from "react-hook-form";
import { EditorRefApi } from "@plane/editor";
// plane types
import { TSticky } from "@plane/types";
// plane utils
import { isCommentEmpty } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store";
// components
import { StickyEditor } from "../../editor";
type TProps = {
stickyData: TSticky | undefined;
stickyData: Partial<TSticky> | undefined;
workspaceSlug: string;
handleUpdate: (payload: Partial<TSticky>) => void;
stickyId: string | undefined;
@@ -67,7 +69,11 @@ export const StickyInput = (props: TProps) => {
onChange(description_html);
handleSubmit(handleFormSubmit)();
}}
placeholder="Click to type here"
placeholder={(_, value) => {
const isContentEmpty = isCommentEmpty(value);
if (!isContentEmpty) return "";
return "Click to type here";
}}
containerClassName="px-0 text-base min-h-[250px] w-full"
uploadFile={async () => ""}
showToolbar={showToolbar}

View File

@@ -12,7 +12,7 @@ import { useSticky } from "@/hooks/use-stickies";
import { STICKY_COLORS_LIST } from "../../editor/sticky-editor/color-palette";
import { StickyDeleteModal } from "../delete-modal";
import { StickyInput } from "./inputs";
import { useStickyOperations } from "./use-operations";
import { getRandomStickyColor, useStickyOperations } from "./use-operations";
type TProps = {
onClose?: () => void;
@@ -33,7 +33,7 @@ export const StickyNote = observer((props: TProps) => {
// sticky operations
const { stickyOperations } = useStickyOperations({ workspaceSlug });
// derived values
const stickyData = stickyId ? stickies[stickyId] : undefined;
const stickyData: Partial<TSticky> = stickyId ? stickies[stickyId] : { background_color: getRandomStickyColor() };
// const isStickiesPage = pathName?.includes("stickies");
const backgroundColor =
STICKY_COLORS_LIST.find((c) => c.key === stickyData?.background_color)?.backgroundColor ||
@@ -45,6 +45,7 @@ export const StickyNote = observer((props: TProps) => {
await stickyOperations.update(stickyId, payload);
} else {
await stickyOperations.create({
...stickyData,
...payload,
});
}
@@ -73,7 +74,7 @@ export const StickyNote = observer((props: TProps) => {
handleClose={() => setIsDeleteModalOpen(false)}
/>
<div
className={cn("w-full flex flex-col h-fit rounded p-4 group/sticky", className)}
className={cn("w-full flex flex-col h-fit rounded p-4 group/sticky max-h-[650px] overflow-y-scroll", className)}
style={{
backgroundColor,
}}

View File

@@ -21,12 +21,13 @@ export class StickyService extends APIService {
async getStickies(
workspaceSlug: string,
cursor: string,
query?: string
query?: string,
per_page?: number
): Promise<{ results: TSticky[]; total_pages: number }> {
return this.get(`/api/workspaces/${workspaceSlug}/stickies/`, {
params: {
cursor,
per_page: STICKIES_PER_PAGE,
per_page: per_page || STICKIES_PER_PAGE,
query,
},
})

View File

@@ -96,7 +96,7 @@ export class StickyStore implements IStickyStore {
};
fetchRecentSticky = async (workspaceSlug: string) => {
const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0");
const response = await this.stickyService.getStickies(workspaceSlug, "1:0:0", undefined, 1);
runInAction(() => {
this.recentStickyId = response.results[0]?.id;
this.stickies[response.results[0]?.id] = response.results[0];