diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index b31185b4eb..79478e733e 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -73,6 +73,8 @@ from .issue import ( IssueReactionLiteSerializer, IssueAttachmentLiteSerializer, IssueLinkLiteSerializer, + IssueVersionDetailSerializer, + IssueDescriptionVersionDetailSerializer, ) from .module import ( diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 1ddc9055b1..8d82691d77 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -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"] diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 625c902011..28cb534e1a 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -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//projects//issues//versions/", + IssueVersionEndpoint.as_view(), + name="page-versions", + ), + path( + "workspaces//projects//issues//versions//", + IssueVersionEndpoint.as_view(), + name="page-versions", + ), + path( + "workspaces//projects//issues//description-versions/", + IssueDescriptionVersionEndpoint.as_view(), + name="page-versions", + ), + path( + "workspaces//projects//issues//description-versions//", + IssueDescriptionVersionEndpoint.as_view(), + name="page-versions", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 3f991792b8..08e3b279b6 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -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, diff --git a/apiserver/plane/app/views/issue/version.py b/apiserver/plane/app/views/issue/version.py new file mode 100644 index 0000000000..ab26ca5a65 --- /dev/null +++ b/apiserver/plane/app/views/issue/version.py @@ -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) diff --git a/web/core/components/editor/sticky-editor/color-palette.tsx b/web/core/components/editor/sticky-editor/color-palette.tsx index f6586121af..a4fe05b777 100644 --- a/web/core/components/editor/sticky-editor/color-palette.tsx +++ b/web/core/components/editor/sticky-editor/color-palette.tsx @@ -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", diff --git a/web/core/components/editor/sticky-editor/editor.tsx b/web/core/components/editor/sticky-editor/editor.tsx index 376a317c8c..d31090b516 100644 --- a/web/core/components/editor/sticky-editor/editor.tsx +++ b/web/core/components/editor/sticky-editor/editor.tsx @@ -43,7 +43,6 @@ export const StickyEditor = React.forwardRef <>, }} - placeholder={placeholder} containerClassName={cn(containerClassName, "relative")} {...rest} /> diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/comment-block.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/comment-block.tsx index 659d642471..fe463077eb 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/comment-block.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/comments/comment-block.tsx @@ -57,9 +57,11 @@ export const IssueCommentBlock: FC = observer((props) => { > {comment.actor_detail.avatar_url && comment.actor_detail.avatar_url !== "" ? ( { = 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)} )}
= observer((props) => {
- {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}
commented {calculateTimeAgo(comment.created_at)}
diff --git a/web/core/components/stickies/sticky/inputs.tsx b/web/core/components/stickies/sticky/inputs.tsx index e56f6927e0..0aef502a10 100644 --- a/web/core/components/stickies/sticky/inputs.tsx +++ b/web/core/components/stickies/sticky/inputs.tsx @@ -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 | undefined; workspaceSlug: string; handleUpdate: (payload: Partial) => 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} diff --git a/web/core/components/stickies/sticky/root.tsx b/web/core/components/stickies/sticky/root.tsx index bee67f913c..50605184b4 100644 --- a/web/core/components/stickies/sticky/root.tsx +++ b/web/core/components/stickies/sticky/root.tsx @@ -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 = 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)} />
{ return this.get(`/api/workspaces/${workspaceSlug}/stickies/`, { params: { cursor, - per_page: STICKIES_PER_PAGE, + per_page: per_page || STICKIES_PER_PAGE, query, }, }) diff --git a/web/core/store/sticky/sticky.store.ts b/web/core/store/sticky/sticky.store.ts index e3d292173e..0872551758 100644 --- a/web/core/store/sticky/sticky.store.ts +++ b/web/core/store/sticky/sticky.store.ts @@ -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];