diff --git a/.github/workflows/build-branch-cloud.yml b/.github/workflows/build-branch-cloud.yml index 84081a59c4..2a01dc9e4b 100644 --- a/.github/workflows/build-branch-cloud.yml +++ b/.github/workflows/build-branch-cloud.yml @@ -15,6 +15,11 @@ on: description: "Release Version" type: string default: v0.0.0-cloud + useVaultSecrets: + description: "Use Vault Secrets" + type: boolean + default: false + required: true isPrerelease: description: "Is Pre-release" type: boolean @@ -113,6 +118,7 @@ jobs: - name: Tailscale uses: tailscale/github-action@v2 + if: ${{github.event.inputs.useVaultSecrets == 'true'}} with: oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} @@ -120,6 +126,7 @@ jobs: - name: Get the ENV values from Vault id: get_vault_secrets + if: ${{github.event.inputs.useVaultSecrets == 'true'}} run: | if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then ENV_NAME="prod" diff --git a/apiserver/plane/authentication/views/app/mobile/signout.py b/apiserver/plane/authentication/views/app/mobile/signout.py index 28f0b50305..7dd7537f0e 100644 --- a/apiserver/plane/authentication/views/app/mobile/signout.py +++ b/apiserver/plane/authentication/views/app/mobile/signout.py @@ -10,9 +10,14 @@ from rest_framework.views import APIView # Module imports from plane.authentication.utils.host import user_ip from plane.db.models import User +from plane.app.authentication.session import BaseSessionAuthentication class MobileSignOutAuthEndpoint(APIView): + authentication_classes = [ + BaseSessionAuthentication, + ] + def post(self, request): # Get user try: diff --git a/apiserver/plane/graphql/queries/cycle.py b/apiserver/plane/graphql/queries/cycle.py index 3ef1531565..06bfcf11e4 100644 --- a/apiserver/plane/graphql/queries/cycle.py +++ b/apiserver/plane/graphql/queries/cycle.py @@ -9,7 +9,7 @@ from asgiref.sync import sync_to_async # Django Imports from django.utils import timezone -from django.db.models import Q +from django.db.models import Q, Exists, OuterRef # Strawberry Imports from strawberry.types import Info @@ -17,7 +17,7 @@ from strawberry.scalars import JSON from strawberry.permission import PermissionExtension # Module Imports -from plane.db.models import Cycle, Issue, CycleUserProperties +from plane.db.models import Cycle, Issue, CycleUserProperties, UserFavorite from plane.graphql.types.cycle import CycleType, CycleUserPropertyType from plane.graphql.types.issue import ( IssuesInformationType, @@ -43,6 +43,13 @@ class CycleQuery: slug: str, project: strawberry.ID, ) -> list[CycleType]: + subquery = UserFavorite.objects.filter( + user=info.context.user, + entity_type="cycle", + entity_identifier=OuterRef("pk"), + project_id=project, + ) + # get cycles those are current and upcoming cycles based on the start_date and end_date cycles = await sync_to_async(list)( Cycle.objects.filter(workspace__slug=slug, project_id=project) @@ -62,6 +69,7 @@ class CycleQuery: ) ) .order_by("start_date") + .annotate(is_favorite=Exists(subquery)) ) return cycles diff --git a/apiserver/plane/graphql/queries/issues/search.py b/apiserver/plane/graphql/queries/issues/search.py index 244c868185..d08ca6fec4 100644 --- a/apiserver/plane/graphql/queries/issues/search.py +++ b/apiserver/plane/graphql/queries/issues/search.py @@ -20,6 +20,14 @@ from strawberry.permission import PermissionExtension from plane.graphql.permissions.workspace import WorkspaceBasePermission from plane.db.models import Issue from plane.graphql.types.issue import IssueLiteType +from plane.graphql.types.paginator import PaginatorResponse +from plane.graphql.utils.paginator import paginate + + +@sync_to_async +def get_issue_details(issue_id): + issue = Issue.objects.get(id=issue_id) + return issue @strawberry.type @@ -43,7 +51,8 @@ class IssuesSearchQuery: relationType: Optional[bool] = False, subIssues: Optional[bool] = False, search: Optional[str] = None, - ) -> list[IssueLiteType]: + cursor: Optional[str] = None, + ) -> PaginatorResponse[IssueLiteType]: issue_queryset = Issue.issue_objects.filter( workspace__slug=slug, project__project_projectmember__member=info.context.user, @@ -81,15 +90,13 @@ class IssuesSearchQuery: # sub issues if subIssues and issue: - current_issue = await sync_to_async(Issue.issue_objects.get)( - pk=issue - ) + current_issue = await get_issue_details(issue) issue_queryset = issue_queryset.filter( - Q(parent__isNull=True), ~Q(parent=issue) + Q(parent__isnull=True), ~Q(pk=issue) ) if current_issue.parent: issue_queryset = issue_queryset.filter( - ~Q(parent=current_issue.parent) + ~Q(pk=current_issue.parent_id) ) # apply search filter @@ -123,4 +130,8 @@ class IssuesSearchQuery: issue["project_identifier"] = issue["project__identifier"] del issue["project__identifier"] - return [IssueLiteType(**issue) for issue in issues] + listed_issues: list[IssueLiteType] = [ + IssueLiteType(**issue) for issue in issues + ] + + return paginate(results_object=listed_issues, cursor=cursor) diff --git a/apiserver/plane/graphql/queries/module.py b/apiserver/plane/graphql/queries/module.py index c0ae696a5c..7574b7988e 100644 --- a/apiserver/plane/graphql/queries/module.py +++ b/apiserver/plane/graphql/queries/module.py @@ -7,13 +7,16 @@ import strawberry # Python Standard Library Imports from asgiref.sync import sync_to_async +# Django Imports +from django.db.models import Exists, OuterRef + # Strawberry Imports from strawberry.types import Info from strawberry.scalars import JSON from strawberry.permission import PermissionExtension # Module Imports -from plane.db.models import Module, Issue, ModuleUserProperties +from plane.db.models import Module, Issue, ModuleUserProperties, UserFavorite from plane.graphql.types.module import ModuleType, ModuleUserPropertyType from plane.graphql.types.issue import ( IssuesInformationType, @@ -40,6 +43,13 @@ class ModuleQuery: project: strawberry.ID, cursor: Optional[str] = None, ) -> PaginatorResponse[ModuleType]: + subquery = UserFavorite.objects.filter( + user=info.context.user, + entity_type="module", + entity_identifier=OuterRef("pk"), + project_id=project, + ) + modules = await sync_to_async(list)( Module.objects.filter(workspace__slug=slug) .filter(project_id=project) @@ -47,6 +57,7 @@ class ModuleQuery: project__project_projectmember__member=info.context.user, project__project_projectmember__is_active=True, ) + .annotate(is_favorite=Exists(subquery)) ) return paginate(results_object=modules, cursor=cursor) diff --git a/apiserver/plane/graphql/queries/workspace.py b/apiserver/plane/graphql/queries/workspace.py index 29c6bd8944..28a5f351e6 100644 --- a/apiserver/plane/graphql/queries/workspace.py +++ b/apiserver/plane/graphql/queries/workspace.py @@ -169,12 +169,11 @@ class YourWorkQuery: # issues issues = await sync_to_async(list)( - Issue.objects.filter(workspace__slug=slug) + Issue.issue_objects.filter(workspace__slug=slug) .filter( Q( project__project_projectmember__member=info.context.user, project__project_projectmember__is_active=True, - # state__group__in=["unstarted", "started"], assignees__in=[info.context.user], ), ) diff --git a/apiserver/plane/graphql/types/cycle.py b/apiserver/plane/graphql/types/cycle.py index fe025d9b53..5520428528 100644 --- a/apiserver/plane/graphql/types/cycle.py +++ b/apiserver/plane/graphql/types/cycle.py @@ -39,6 +39,7 @@ class CycleType: updated_at: datetime total_issues: int completed_issues: int + is_favorite: bool owned_by: Optional[UserType] @strawberry.field diff --git a/apiserver/plane/graphql/types/module.py b/apiserver/plane/graphql/types/module.py index 6e6ee12127..5e27dc20a2 100644 --- a/apiserver/plane/graphql/types/module.py +++ b/apiserver/plane/graphql/types/module.py @@ -41,6 +41,7 @@ class ModuleType: logo_props: Optional[JSON] total_issues: int completed_issues: int + is_favorite: bool lead: Optional[UserType] @strawberry.field diff --git a/web/ee/components/mobile/authentication/forms/email-confirmation.tsx b/web/ee/components/mobile/authentication/forms/email-confirmation.tsx index 5877d7e23e..69ac121531 100644 --- a/web/ee/components/mobile/authentication/forms/email-confirmation.tsx +++ b/web/ee/components/mobile/authentication/forms/email-confirmation.tsx @@ -14,6 +14,8 @@ import { } from "@/helpers/authentication.helper"; import { cn } from "@/helpers/common.helper"; import { checkEmailValidity } from "@/helpers/string.helper"; +// plane web services +import mobileAuthService from "@/plane-web/services/mobile.service"; // services import { AuthService } from "@/services/auth.service"; @@ -47,9 +49,28 @@ export const MobileAuthEmailValidationForm: FC = const handleFormSubmit = async (event: FormEvent) => { event.preventDefault(); + let isUserSignOut = false; + try { + await mobileAuthService.currentUser(); + isUserSignOut = true; + } catch (error) { + console.error(error); + } + + let isEmailShouldBeVerified = true; + if (isUserSignOut) { + try { + await mobileAuthService.signOut(); + } catch (error) { + console.error(error); + isEmailShouldBeVerified = false; + } + } + + if (!isEmailShouldBeVerified) return; + handleEmail(email); setIsSubmitting(true); - const payload: IEmailCheckData = { email: email, }; diff --git a/web/ee/components/mobile/authentication/root.tsx b/web/ee/components/mobile/authentication/root.tsx index 32c6c49bff..a8c965c426 100644 --- a/web/ee/components/mobile/authentication/root.tsx +++ b/web/ee/components/mobile/authentication/root.tsx @@ -3,6 +3,8 @@ import { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; +// components +import { AuthBanner } from "@/components/account"; // helpers import { authErrorHandler, @@ -11,12 +13,8 @@ import { EErrorAlertType, TAuthErrorInfo, } from "@/helpers/authentication.helper"; -// components -import { AuthBanner } from "@/components/account"; // hooks import { useInstance } from "@/hooks/store"; -// services -import { AuthService } from "@/services/auth.service"; // plane web components import { MobileTermsAndConditions, @@ -24,6 +22,8 @@ import { MobileAuthUniqueCodeForm, MobileAuthPasswordForm, } from "@/plane-web/components/mobile"; +// services +import { AuthService } from "@/services/auth.service"; // service initialization const authService = new AuthService(); @@ -53,7 +53,7 @@ const UNIQUE_CODE_ERROR_CODES = [ const PASSWORD_ERROR_CODES = [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]; -export const AuthRoot: FC = observer((props) => { +export const AuthRoot: FC = observer(() => { // router const searchParams = useSearchParams(); // query params diff --git a/web/ee/services/mobile.service.ts b/web/ee/services/mobile.service.ts new file mode 100644 index 0000000000..e05756f82d --- /dev/null +++ b/web/ee/services/mobile.service.ts @@ -0,0 +1,36 @@ +import axios, { AxiosInstance } from "axios"; +import { IUser } from "@plane/types"; +// helpers +import { API_BASE_URL } from "@/helpers/common.helper"; + +export class MobileAuthService { + axiosInstance: AxiosInstance; + constructor() { + this.axiosInstance = axios.create({ + baseURL: API_BASE_URL, + withCredentials: true, + }); + } + + async currentUser(): Promise { + return this.axiosInstance + .get("/api/users/me/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async signOut(): Promise { + return this.axiosInstance + .post("/auth/mobile/sign-out/", {}) + .then((response) => response.data) + .catch((error) => { + throw error; + }); + } +} + +const mobileAuthService = new MobileAuthService(); + +export default mobileAuthService;