diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 10b0182d6c..72c5f8da98 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -10,6 +10,7 @@ from .issue import ( IssueAttachmentSerializer, IssueActivitySerializer, IssueExpandSerializer, + IssueLiteSerializer, ) from .state import StateLiteSerializer, StateSerializer from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer diff --git a/apiserver/plane/api/serializers/base.py b/apiserver/plane/api/serializers/base.py index 5b68a71138..5653ba318a 100644 --- a/apiserver/plane/api/serializers/base.py +++ b/apiserver/plane/api/serializers/base.py @@ -67,6 +67,7 @@ class BaseSerializer(serializers.ModelSerializer): # Import all the expandable serializers from . import ( IssueSerializer, + IssueLiteSerializer, ProjectLiteSerializer, StateLiteSerializer, UserLiteSerializer, @@ -86,6 +87,7 @@ class BaseSerializer(serializers.ModelSerializer): "actor": UserLiteSerializer, "owned_by": UserLiteSerializer, "members": UserLiteSerializer, + "parent": IssueLiteSerializer, } # Check if field in expansion then expand the field if expand in expansion: diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 31a832f7b6..7f8712bf65 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -274,6 +274,17 @@ class IssueSerializer(BaseSerializer): return data +class IssueLiteSerializer(BaseSerializer): + class Meta: + model = Issue + fields = [ + "id", + "sequence_id", + "project_id", + ] + read_only_fields = fields + + class LabelSerializer(BaseSerializer): class Meta: model = Label diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 0189316cf3..44f3250156 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -128,7 +128,9 @@ def extract_mentions(issue_instance): "mention-component", attrs={"target": "users"} ) - mentions = [mention_tag["entity_identifier"] for mention_tag in mention_tags] + mentions = [ + mention_tag["entity_identifier"] for mention_tag in mention_tags + ] return list(set(mentions)) except Exception: @@ -198,6 +200,16 @@ def create_mention_notification( "actor": str(activity.get("actor_id")), "new_value": str(activity.get("new_value")), "old_value": str(activity.get("old_value")), + "old_identifier": ( + str(activity.get("old_identifier")) + if activity.get("old_identifier") + else None + ), + "new_identifier": ( + str(activity.get("new_identifier")) + if activity.get("new_identifier") + else None + ), }, }, ) @@ -440,6 +452,24 @@ def notifications( if issue_comment is not None else "" ), + "old_identifier": ( + str( + issue_activity.get( + "old_identifier" + ) + ) + if issue_activity.get("old_identifier") + else None + ), + "new_identifier": ( + str( + issue_activity.get( + "new_identifier" + ) + ) + if issue_activity.get("new_identifier") + else None + ), }, }, ) @@ -489,6 +519,28 @@ def notifications( if issue_comment is not None else "" ), + "old_identifier": ( + str( + issue_activity.get( + "old_identifier" + ) + ) + if issue_activity.get( + "old_identifier" + ) + else None + ), + "new_identifier": ( + str( + issue_activity.get( + "new_identifier" + ) + ) + if issue_activity.get( + "new_identifier" + ) + else None + ), "activity_time": issue_activity.get( "created_at" ), @@ -572,6 +624,28 @@ def notifications( "old_value": str( issue_activity.get("old_value") ), + "old_identifier": ( + str( + issue_activity.get( + "old_identifier" + ) + ) + if issue_activity.get( + "old_identifier" + ) + else None + ), + "new_identifier": ( + str( + issue_activity.get( + "new_identifier" + ) + ) + if issue_activity.get( + "new_identifier" + ) + else None + ), "activity_time": issue_activity.get( "created_at" ), @@ -627,6 +701,28 @@ def notifications( "old_value": str( last_activity.old_value ), + "old_identifier": ( + str( + issue_activity.get( + "old_identifier" + ) + ) + if issue_activity.get( + "old_identifier" + ) + else None + ), + "new_identifier": ( + str( + issue_activity.get( + "new_identifier" + ) + ) + if issue_activity.get( + "new_identifier" + ) + else None + ), }, }, ) @@ -662,7 +758,31 @@ def notifications( "old_value": str( last_activity.old_value ), - "activity_time": str(last_activity.created_at), + "old_identifier": ( + str( + issue_activity.get( + "old_identifier" + ) + ) + if issue_activity.get( + "old_identifier" + ) + else None + ), + "new_identifier": ( + str( + issue_activity.get( + "new_identifier" + ) + ) + if issue_activity.get( + "new_identifier" + ) + else None + ), + "activity_time": str( + last_activity.created_at + ), }, }, ) @@ -719,6 +839,28 @@ def notifications( "old_value" ) ), + "old_identifier": ( + str( + issue_activity.get( + "old_identifier" + ) + ) + if issue_activity.get( + "old_identifier" + ) + else None + ), + "new_identifier": ( + str( + issue_activity.get( + "new_identifier" + ) + ) + if issue_activity.get( + "new_identifier" + ) + else None + ), "activity_time": issue_activity.get( "created_at" ), diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 113746e3fb..5503521bea 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -2,10 +2,8 @@ # Python imports import os -import ssl from urllib.parse import urlparse -import certifi # Third party imports import dj_database_url diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index bd955ef4f3..096310aeb9 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -1,20 +1,17 @@ "use client"; -import { useState } from "react"; import { observer } from "mobx-react"; -import { CircleAlert, Sparkle } from "lucide-react"; +import { CircleAlert } from "lucide-react"; // editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // ui -import { ArchiveIcon, Tooltip } from "@plane/ui"; +import { ArchiveIcon, FavoriteStar, setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // components -import { GptAssistantPopover } from "@/components/core"; import { LockedComponent } from "@/components/icons/locked-component"; import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; // hooks -import { useInstance } from "@/hooks/store"; import useOnlineStatus from "@/hooks/use-online-status"; // store import { IPage } from "@/store/pages/page"; @@ -29,18 +26,37 @@ type Props = { export const PageExtraOptions: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, hasConnectionFailed, page, readOnlyEditorRef } = props; - // states - const [gptModalOpen, setGptModal] = useState(false); - // store hooks - const { config } = useInstance(); // derived values - const { archived_at, isContentEditable, is_locked } = page; + const { + archived_at, + isContentEditable, + is_favorite, + is_locked, + canCurrentUserFavoritePage, + addToFavorites, + removePageFromFavorites, + } = page; // use online status const { isOnline } = useOnlineStatus(); - - const handleAiAssistance = async (response: string) => { - if (!editorRef) return; - editorRef.current?.setEditorValueAtCursorPosition(response); + // favorite handler + const handleFavorite = () => { + if (is_favorite) { + removePageFromFavorites().then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Page removed from favorites.", + }) + ); + } else { + addToFavorites().then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Page added to favorites.", + }) + ); + } }; return ( @@ -74,28 +90,8 @@ export const PageExtraOptions: React.FC = observer((props) => { )} - {isContentEditable && config?.has_openai_configured && ( - { - setGptModal((prevData) => !prevData); - // this is done so that the title do not reset after gpt popover closed - // reset(getValues()); - }} - onResponse={handleAiAssistance} - placement="top-end" - button={ - - } - className="!min-w-[38rem]" - /> + {canCurrentUserFavoritePage && ( + )} = observer((props) => { const { workspaceSlug, projectId, pageId, parentRef } = props; - // store hooks const page = usePage(pageId); const { getUserDetails } = useMember(); - const { getProjectById } = useProject(); - // derived values - const { access, created_at, is_favorite, owned_by, addToFavorites, removePageFromFavorites } = page; - - // derived values - const project = getProjectById(projectId); - const isViewerOrGuest = - project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role); + const { + access, + created_at, + is_favorite, + owned_by, + canCurrentUserFavoritePage, + addToFavorites, + removePageFromFavorites, + } = page; const ownerDetails = owned_by ? getUserDetails(owned_by) : undefined; // handlers const handleFavorites = () => { - if (is_favorite) + if (is_favorite) { removePageFromFavorites().then(() => setToast({ type: TOAST_TYPE.SUCCESS, @@ -48,7 +46,7 @@ export const BlockItemAction: FC = observer((props) => { message: "Page removed from favorites.", }) ); - else + } else { addToFavorites().then(() => setToast({ type: TOAST_TYPE.SUCCESS, @@ -56,7 +54,9 @@ export const BlockItemAction: FC = observer((props) => { message: "Page added to favorites.", }) ); + } }; + return ( <> {/* page details */} @@ -81,7 +81,7 @@ export const BlockItemAction: FC = observer((props) => { {/* favorite/unfavorite */} - {!isViewerOrGuest && ( + {canCurrentUserFavoritePage && ( { e.preventDefault(); diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index 4709691265..d7f28c0f88 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -24,6 +24,7 @@ export interface IPage extends TPage { canCurrentUserChangeAccess: boolean; canCurrentUserArchivePage: boolean; canCurrentUserDeletePage: boolean; + canCurrentUserFavoritePage: boolean; isContentEditable: boolean; // helpers oldName: string; @@ -133,6 +134,7 @@ export class Page implements IPage { canCurrentUserChangeAccess: computed, canCurrentUserArchivePage: computed, canCurrentUserDeletePage: computed, + canCurrentUserFavoritePage: computed, isContentEditable: computed, // actions update: action, @@ -255,6 +257,14 @@ export class Page implements IPage { return this.isCurrentUserOwner || currentUserProjectRole === EUserProjectRoles.ADMIN; } + /** + * @description returns true if the current logged in user can favorite the page + */ + get canCurrentUserFavoritePage() { + const currentUserProjectRole = this.store.user.membership.currentProjectRole; + return !!currentUserProjectRole && currentUserProjectRole >= EUserProjectRoles.MEMBER; + } + /** * @description returns true if the page can be edited */