diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index f2bf4259f..000000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-# This configuration file was automatically generated by Gitpod.
-# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
-# and commit this file to your remote git repository to share the goodness with others.
-
-# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
-
-tasks:
- - init: yarn install && yarn run build
- command: yarn run start
-
-
diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py
index 3dca6c312..253da2c5b 100644
--- a/apiserver/plane/api/views/cycle.py
+++ b/apiserver/plane/api/views/cycle.py
@@ -333,13 +333,21 @@ class CycleViewSet(BaseViewSet):
workspace__slug=slug, project_id=project_id, pk=pk
)
+ request_data = request.data
+
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
- return Response(
- {
- "error": "The Cycle has already been completed so it cannot be edited"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
+ if "sort_order" in request_data:
+ # Can only change sort order
+ request_data = {
+ "sort_order": request_data.get("sort_order", cycle.sort_order)
+ }
+ else:
+ return Response(
+ {
+ "error": "The Cycle has already been completed so it cannot be edited"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid():
@@ -373,7 +381,9 @@ class CycleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.annotate(display_name=F("assignees__display_name"))
- .values("first_name", "last_name", "assignee_id", "avatar", "display_name")
+ .values(
+ "first_name", "last_name", "assignee_id", "avatar", "display_name"
+ )
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -709,7 +719,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
class CycleFavoriteViewSet(BaseViewSet):
-
serializer_class = CycleFavoriteSerializer
model = CycleFavorite
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index 3d6b59c7f..a390f7b81 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -17,6 +17,7 @@ from django.db.models import (
When,
Exists,
Max,
+ IntegerField,
)
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.decorators import method_decorator
@@ -337,7 +338,11 @@ class UserWorkSpaceIssues(BaseAPIView):
issue_queryset = (
Issue.issue_objects.filter(
- (Q(assignees__in=[request.user]) | Q(created_by=request.user) | Q(issue_subscribers__subscriber=request.user)),
+ (
+ Q(assignees__in=[request.user])
+ | Q(created_by=request.user)
+ | Q(issue_subscribers__subscriber=request.user)
+ ),
workspace__slug=slug,
)
.annotate(
@@ -1994,7 +1999,9 @@ class IssueVotePublicViewSet(BaseViewSet):
serializer = IssueVoteSerializer(issue_vote)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except IntegrityError:
- return Response({"error": "Reaction already exists"}, status=status.HTTP_400_BAD_REQUEST)
+ return Response(
+ {"error": "Reaction already exists"}, status=status.HTTP_400_BAD_REQUEST
+ )
except Exception as e:
capture_exception(e)
return Response(
@@ -2172,9 +2179,32 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
issues = IssuePublicSerializer(issue_queryset, many=True).data
- states = State.objects.filter(
- workspace__slug=slug, project_id=project_id
- ).values("name", "group", "color", "id")
+ state_group_order = [
+ "backlog",
+ "unstarted",
+ "started",
+ "completed",
+ "cancelled",
+ ]
+
+ states = (
+ State.objects.filter(
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(
+ custom_order=Case(
+ *[
+ When(group=value, then=Value(index))
+ for index, value in enumerate(state_group_order)
+ ],
+ default=Value(len(state_group_order)),
+ output_field=IntegerField(),
+ ),
+ )
+ .values("name", "group", "color", "id")
+ .order_by("custom_order", "sequence")
+ )
labels = Label.objects.filter(
workspace__slug=slug, project_id=project_id
diff --git a/space/.env.example b/space/.env.example
index 4fb0e4df6..2d3165893 100644
--- a/space/.env.example
+++ b/space/.env.example
@@ -1 +1,8 @@
-NEXT_PUBLIC_API_BASE_URL=''
\ No newline at end of file
+# Base url for the API requests
+NEXT_PUBLIC_API_BASE_URL=""
+# Public boards deploy URL
+NEXT_PUBLIC_DEPLOY_URL="https://plane-space-dev.vercel.app"
+# Google Client ID for Google OAuth
+NEXT_PUBLIC_GOOGLE_CLIENTID=232920797020-235n93bn7hh7628vdd69hq873129ng4o.apps.googleusercontent.com
+# Flag to toggle OAuth
+NEXT_PUBLIC_ENABLE_OAUTH=1
\ No newline at end of file
diff --git a/space/components/accounts/index.ts b/space/components/accounts/index.ts
index 093e8538c..03a173766 100644
--- a/space/components/accounts/index.ts
+++ b/space/components/accounts/index.ts
@@ -4,3 +4,5 @@ export * from "./email-reset-password-form";
export * from "./github-login-button";
export * from "./google-login";
export * from "./onboarding-form";
+export * from "./sign-in";
+export * from "./user-logged-in";
diff --git a/space/components/accounts/sign-in.tsx b/space/components/accounts/sign-in.tsx
new file mode 100644
index 000000000..50d9c7da0
--- /dev/null
+++ b/space/components/accounts/sign-in.tsx
@@ -0,0 +1,156 @@
+import React from "react";
+
+import Image from "next/image";
+import { useRouter } from "next/router";
+
+// mobx
+import { observer } from "mobx-react-lite";
+import { useMobxStore } from "lib/mobx/store-provider";
+// services
+import authenticationService from "services/authentication.service";
+// hooks
+import useToast from "hooks/use-toast";
+// components
+import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
+// images
+import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.svg";
+
+export const SignInView = observer(() => {
+ const { user: userStore } = useMobxStore();
+
+ const router = useRouter();
+ const { next_path } = router.query;
+
+ const { setToastAlert } = useToast();
+
+ const onSignInError = (error: any) => {
+ setToastAlert({
+ title: "Error signing in!",
+ type: "error",
+ message: error?.error || "Something went wrong. Please try again later or contact the support team.",
+ });
+ };
+
+ const onSignInSuccess = (response: any) => {
+ const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
+
+ userStore.setCurrentUser(response?.user);
+
+ if (!isOnboarded) {
+ router.push(`/onboarding?next_path=${next_path}`);
+ return;
+ }
+ router.push((next_path ?? "/").toString());
+ };
+
+ const handleGoogleSignIn = async ({ clientId, credential }: any) => {
+ try {
+ if (clientId && credential) {
+ const socialAuthPayload = {
+ medium: "google",
+ credential,
+ clientId,
+ };
+ const response = await authenticationService.socialAuth(socialAuthPayload);
+
+ onSignInSuccess(response);
+ } else {
+ throw Error("Cant find credentials");
+ }
+ } catch (err: any) {
+ onSignInError(err);
+ }
+ };
+
+ const handleGitHubSignIn = async (credential: string) => {
+ try {
+ if (process.env.NEXT_PUBLIC_GITHUB_ID && credential) {
+ const socialAuthPayload = {
+ medium: "github",
+ credential,
+ clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
+ };
+ const response = await authenticationService.socialAuth(socialAuthPayload);
+ onSignInSuccess(response);
+ } else {
+ throw Error("Cant find credentials");
+ }
+ } catch (err: any) {
+ onSignInError(err);
+ }
+ };
+
+ const handlePasswordSignIn = async (formData: any) => {
+ await authenticationService
+ .emailLogin(formData)
+ .then((response) => {
+ try {
+ if (response) {
+ onSignInSuccess(response);
+ }
+ } catch (err: any) {
+ onSignInError(err);
+ }
+ })
+ .catch((err) => onSignInError(err));
+ };
+
+ const handleEmailCodeSignIn = async (response: any) => {
+ try {
+ if (response) {
+ onSignInSuccess(response);
+ }
+ } catch (err: any) {
+ onSignInError(err);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
+ <>
+
+ Sign in to Plane
+
+
+ >
+ ) : (
+
+ )}
+
+ {parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
+
+ By signing up, you agree to the{" "}
+
+ Terms & Conditions
+
+
+ ) : null}
+
+
+
+ );
+});
diff --git a/space/components/accounts/user-logged-in.tsx b/space/components/accounts/user-logged-in.tsx
new file mode 100644
index 000000000..3f177bcc8
--- /dev/null
+++ b/space/components/accounts/user-logged-in.tsx
@@ -0,0 +1,51 @@
+import Image from "next/image";
+
+// mobx
+import { useMobxStore } from "lib/mobx/store-provider";
+// assets
+import UserLoggedInImage from "public/user-logged-in.svg";
+import PlaneLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
+
+export const UserLoggedIn = () => {
+ const { user: userStore } = useMobxStore();
+ const user = userStore.currentUser;
+
+ if (!user) return null;
+
+ return (
+
+
+
+
+
+
+ {user.avatar && user.avatar !== "" ? (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+ ) : (
+
+ {(user.display_name ?? "U")[0]}
+
+ )}
+
{user.display_name}
+
+
+
+
+
+
+
Logged in Successfully!
+
+ You{"'"}ve successfully logged in. Please enter the appropriate project URL to view the issue board.
+
+
+
+
+ );
+};
diff --git a/space/components/icons/index.ts b/space/components/icons/index.ts
index 5f23e0f3a..28162f591 100644
--- a/space/components/icons/index.ts
+++ b/space/components/icons/index.ts
@@ -1,5 +1 @@
-export * from "./issue-group/backlog-state-icon";
-export * from "./issue-group/unstarted-state-icon";
-export * from "./issue-group/started-state-icon";
-export * from "./issue-group/completed-state-icon";
-export * from "./issue-group/cancelled-state-icon";
+export * from "./state-group";
diff --git a/space/components/icons/issue-group/backlog-state-icon.tsx b/space/components/icons/state-group/backlog-state-icon.tsx
similarity index 100%
rename from space/components/icons/issue-group/backlog-state-icon.tsx
rename to space/components/icons/state-group/backlog-state-icon.tsx
diff --git a/space/components/icons/issue-group/cancelled-state-icon.tsx b/space/components/icons/state-group/cancelled-state-icon.tsx
similarity index 100%
rename from space/components/icons/issue-group/cancelled-state-icon.tsx
rename to space/components/icons/state-group/cancelled-state-icon.tsx
diff --git a/space/components/icons/issue-group/completed-state-icon.tsx b/space/components/icons/state-group/completed-state-icon.tsx
similarity index 100%
rename from space/components/icons/issue-group/completed-state-icon.tsx
rename to space/components/icons/state-group/completed-state-icon.tsx
diff --git a/space/components/icons/state-group/index.ts b/space/components/icons/state-group/index.ts
new file mode 100644
index 000000000..6ede38df6
--- /dev/null
+++ b/space/components/icons/state-group/index.ts
@@ -0,0 +1,6 @@
+export * from "./backlog-state-icon";
+export * from "./cancelled-state-icon";
+export * from "./completed-state-icon";
+export * from "./started-state-icon";
+export * from "./state-group-icon";
+export * from "./unstarted-state-icon";
diff --git a/space/components/icons/issue-group/started-state-icon.tsx b/space/components/icons/state-group/started-state-icon.tsx
similarity index 100%
rename from space/components/icons/issue-group/started-state-icon.tsx
rename to space/components/icons/state-group/started-state-icon.tsx
diff --git a/space/components/icons/state-group/state-group-icon.tsx b/space/components/icons/state-group/state-group-icon.tsx
new file mode 100644
index 000000000..1af523400
--- /dev/null
+++ b/space/components/icons/state-group/state-group-icon.tsx
@@ -0,0 +1,29 @@
+// icons
+import {
+ BacklogStateIcon,
+ CancelledStateIcon,
+ CompletedStateIcon,
+ StartedStateIcon,
+ UnstartedStateIcon,
+} from "components/icons";
+import { TIssueGroupKey } from "types/issue";
+
+type Props = {
+ stateGroup: TIssueGroupKey;
+ color: string;
+ className?: string;
+ height?: string;
+ width?: string;
+};
+
+export const StateGroupIcon: React.FC = ({ stateGroup, className, color, height = "12px", width = "12px" }) => {
+ if (stateGroup === "backlog")
+ return ;
+ else if (stateGroup === "cancelled")
+ return ;
+ else if (stateGroup === "completed")
+ return ;
+ else if (stateGroup === "started")
+ return ;
+ else return ;
+};
diff --git a/space/components/icons/issue-group/unstarted-state-icon.tsx b/space/components/icons/state-group/unstarted-state-icon.tsx
similarity index 100%
rename from space/components/icons/issue-group/unstarted-state-icon.tsx
rename to space/components/icons/state-group/unstarted-state-icon.tsx
diff --git a/space/components/issues/board-views/kanban/header.tsx b/space/components/issues/board-views/kanban/header.tsx
index 69c252593..5645e2b3b 100644
--- a/space/components/issues/board-views/kanban/header.tsx
+++ b/space/components/issues/board-views/kanban/header.tsx
@@ -1,11 +1,11 @@
-"use client";
-
// mobx react lite
import { observer } from "mobx-react-lite";
// interfaces
import { IIssueState } from "types/issue";
// constants
import { issueGroupFilter } from "constants/data";
+// icons
+import { StateGroupIcon } from "components/icons";
// mobx hook
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
@@ -20,7 +20,7 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
return (
-
+
{state?.name}
diff --git a/space/components/issues/board-views/list/block.tsx b/space/components/issues/board-views/list/block.tsx
index 2d1cdf9ba..bdf39b84f 100644
--- a/space/components/issues/board-views/list/block.tsx
+++ b/space/components/issues/board-views/list/block.tsx
@@ -6,15 +6,12 @@ import { IssueBlockPriority } from "components/issues/board-views/block-priority
import { IssueBlockState } from "components/issues/board-views/block-state";
import { IssueBlockLabels } from "components/issues/board-views/block-labels";
import { IssueBlockDueDate } from "components/issues/board-views/block-due-date";
-import { IssueBlockUpVotes } from "components/issues/board-views/block-upvotes";
-import { IssueBlockDownVotes } from "components/issues/board-views/block-downvotes";
// mobx hook
import { useMobxStore } from "lib/mobx/store-provider";
// interfaces
import { IIssue } from "types/issue";
// store
import { RootStore } from "store/root";
-import { IssueVotes } from "components/issues/peek-overview";
export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => {
const { issue } = props;
@@ -40,9 +37,6 @@ export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => {
// router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`);
};
- const totalUpVotes = issue.votes.filter((v) => v.vote === 1);
- const totalDownVotes = issue.votes.filter((v) => v.vote === -1);
-
return (
diff --git a/space/components/issues/board-views/list/header.tsx b/space/components/issues/board-views/list/header.tsx
index 546c20bf6..83312e7b9 100644
--- a/space/components/issues/board-views/list/header.tsx
+++ b/space/components/issues/board-views/list/header.tsx
@@ -1,9 +1,9 @@
-"use client";
-
// mobx react lite
import { observer } from "mobx-react-lite";
// interfaces
import { IIssueState } from "types/issue";
+// icons
+import { StateGroupIcon } from "components/icons";
// constants
import { issueGroupFilter } from "constants/data";
// mobx hook
@@ -20,7 +20,7 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
return (
-
+
{state?.name}
{store.issue.getCountOfIssuesByState(state.id)}
diff --git a/space/components/issues/board-views/list/index.tsx b/space/components/issues/board-views/list/index.tsx
index 4d4701840..1c6900dd9 100644
--- a/space/components/issues/board-views/list/index.tsx
+++ b/space/components/issues/board-views/list/index.tsx
@@ -1,4 +1,3 @@
-import { useEffect } from "react";
import { observer } from "mobx-react-lite";
// components
import { IssueListHeader } from "components/issues/board-views/list/header";
@@ -9,7 +8,6 @@ import { IIssueState, IIssue } from "types/issue";
import { useMobxStore } from "lib/mobx/store-provider";
// store
import { RootStore } from "store/root";
-import { useRouter } from "next/router";
export const IssueListView = observer(() => {
const { issue: issueStore }: RootStore = useMobxStore();
diff --git a/space/components/issues/peek-overview/header.tsx b/space/components/issues/peek-overview/header.tsx
index 79de3978b..2aa43ff47 100644
--- a/space/components/issues/peek-overview/header.tsx
+++ b/space/components/issues/peek-overview/header.tsx
@@ -1,7 +1,5 @@
import React from "react";
-import { useRouter } from "next/router";
-
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// hooks
@@ -48,15 +46,12 @@ export const PeekOverviewHeader: React.FC
= (props) => {
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
- const router = useRouter();
- const { workspace_slug } = router.query;
-
const { setToastAlert } = useToast();
const handleCopyLink = () => {
- const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
+ const urlToCopy = window.location.href;
- copyTextToClipboard(`${originURL}/${workspace_slug}/projects/${issueDetails?.project}/`).then(() => {
+ copyTextToClipboard(urlToCopy).then(() => {
setToastAlert({
type: "success",
title: "Link copied!",
diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx
index 2d454852a..6b3394b56 100644
--- a/space/components/issues/peek-overview/issue-properties.tsx
+++ b/space/components/issues/peek-overview/issue-properties.tsx
@@ -1,21 +1,15 @@
-// headless ui
-import { Disclosure } from "@headlessui/react";
-// import { getStateGroupIcon } from "components/icons";
// hooks
import useToast from "hooks/use-toast";
// icons
import { Icon } from "components/ui";
import { copyTextToClipboard, addSpaceIfCamelCase } from "helpers/string.helper";
+// helpers
+import { renderDateFormat } from "constants/helpers";
// types
import { IIssue } from "types/issue";
+import { IPeekMode } from "store/issue_details";
// constants
import { issueGroupFilter, issuePriorityFilter } from "constants/data";
-import { useEffect } from "react";
-import { renderDateFormat } from "constants/helpers";
-import { IPeekMode } from "store/issue_details";
-import { useRouter } from "next/router";
-import { RootStore } from "store/root";
-import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
issueDetails: IIssue;
@@ -37,11 +31,6 @@ const validDate = (date: any, state: string): string => {
export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mode }) => {
const { setToastAlert } = useToast();
- const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
-
- const router = useRouter();
- const { workspaceSlug } = router.query;
-
const startDate = issueDetails.start_date;
const targetDate = issueDetails.target_date;
@@ -57,11 +46,9 @@ export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mod
const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null;
const handleCopyLink = () => {
- const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
+ const urlToCopy = window.location.href;
- copyTextToClipboard(
- `${originURL}/${workspaceSlug}/projects/${issueDetails.project}/issues/${issueDetails.id}`
- ).then(() => {
+ copyTextToClipboard(urlToCopy).then(() => {
setToastAlert({
type: "success",
title: "Link copied!",
diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx
index 09cfa5b78..a3d7386eb 100644
--- a/space/components/issues/peek-overview/layout.tsx
+++ b/space/components/issues/peek-overview/layout.tsx
@@ -65,26 +65,24 @@ export const IssuePeekOverview: React.FC = observer((props) => {
return (
<>
-
-
+
= observer((props) => {
>
-
-
-
-
- {issueDetailStore.peekMode === "modal" && (
-
- )}
- {issueDetailStore.peekMode === "full" && (
-
- )}
-
-
-
-
+
+
+
+ {issueDetailStore.peekMode === "modal" && (
+
+ )}
+ {issueDetailStore.peekMode === "full" && (
+
+ )}
+
+
+
>
diff --git a/space/components/views/home.tsx b/space/components/views/home.tsx
new file mode 100644
index 000000000..999fce073
--- /dev/null
+++ b/space/components/views/home.tsx
@@ -0,0 +1,13 @@
+// mobx
+import { observer } from "mobx-react-lite";
+import { useMobxStore } from "lib/mobx/store-provider";
+// components
+import { SignInView, UserLoggedIn } from "components/accounts";
+
+export const HomeView = observer(() => {
+ const { user: userStore } = useMobxStore();
+
+ if (!userStore.currentUser) return ;
+
+ return ;
+});
diff --git a/space/components/views/index.ts b/space/components/views/index.ts
new file mode 100644
index 000000000..84d36cd29
--- /dev/null
+++ b/space/components/views/index.ts
@@ -0,0 +1 @@
+export * from "./home";
diff --git a/space/components/views/project-details.tsx b/space/components/views/project-details.tsx
index bbf043130..9a6cd824c 100644
--- a/space/components/views/project-details.tsx
+++ b/space/components/views/project-details.tsx
@@ -55,7 +55,7 @@ export const ProjectDetailsView = observer(() => {
) : (
<>
{issueStore?.error ? (
-
+
Something went wrong.
) : (
@@ -67,7 +67,7 @@ export const ProjectDetailsView = observer(() => {
)}
{projectStore?.activeBoard === "kanban" && (
-
+
)}
diff --git a/space/layouts/project-layout.tsx b/space/layouts/project-layout.tsx
index f19ddabd2..1a0b7899e 100644
--- a/space/layouts/project-layout.tsx
+++ b/space/layouts/project-layout.tsx
@@ -13,18 +13,20 @@ const ProjectLayout = ({ children }: { children: React.ReactNode }) => (
{children}
-
+
+
+
+
+
+
+ Powered by Plane Deploy
+
+
);
diff --git a/space/pages/_app.tsx b/space/pages/_app.tsx
index 2995edbbf..33c137d41 100644
--- a/space/pages/_app.tsx
+++ b/space/pages/_app.tsx
@@ -12,6 +12,8 @@ import MobxStoreInit from "lib/mobx/store-init";
// constants
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "constants/seo";
+const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/spaces/";
+
function MyApp({ Component, pageProps }: AppProps) {
return (
@@ -25,11 +27,11 @@ function MyApp({ Component, pageProps }: AppProps) {
-
-
-
-
-
+
+
+
+
+
diff --git a/space/pages/index.tsx b/space/pages/index.tsx
index 87a291441..fe0b7d33a 100644
--- a/space/pages/index.tsx
+++ b/space/pages/index.tsx
@@ -1,156 +1,8 @@
-import React, { useEffect } from "react";
-import Image from "next/image";
-import { useRouter } from "next/router";
-// assets
-import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.svg";
-// mobx
-import { observer } from "mobx-react-lite";
-import { useMobxStore } from "lib/mobx/store-provider";
-// services
-import authenticationService from "services/authentication.service";
-// hooks
-import useToast from "hooks/use-toast";
+import React from "react";
+
// components
-import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
+import { HomeView } from "components/views";
-const HomePage = () => {
- const { user: userStore } = useMobxStore();
-
- const router = useRouter();
- const { next_path } = router.query;
-
- const { setToastAlert } = useToast();
-
- const onSignInError = (error: any) => {
- setToastAlert({
- title: "Error signing in!",
- type: "error",
- message: error?.error || "Something went wrong. Please try again later or contact the support team.",
- });
- };
-
- const onSignInSuccess = (response: any) => {
- const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
-
- userStore.setCurrentUser(response?.user);
-
- if (!isOnboarded) {
- router.push(`/onboarding?next_path=${next_path}`);
- return;
- }
- router.push((next_path ?? "/").toString());
- };
-
- const handleGoogleSignIn = async ({ clientId, credential }: any) => {
- try {
- if (clientId && credential) {
- const socialAuthPayload = {
- medium: "google",
- credential,
- clientId,
- };
- const response = await authenticationService.socialAuth(socialAuthPayload);
-
- onSignInSuccess(response);
- } else {
- throw Error("Cant find credentials");
- }
- } catch (err: any) {
- onSignInError(err);
- }
- };
-
- const handleGitHubSignIn = async (credential: string) => {
- try {
- if (process.env.NEXT_PUBLIC_GITHUB_ID && credential) {
- const socialAuthPayload = {
- medium: "github",
- credential,
- clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
- };
- const response = await authenticationService.socialAuth(socialAuthPayload);
- onSignInSuccess(response);
- } else {
- throw Error("Cant find credentials");
- }
- } catch (err: any) {
- onSignInError(err);
- }
- };
-
- const handlePasswordSignIn = async (formData: any) => {
- await authenticationService
- .emailLogin(formData)
- .then((response) => {
- try {
- if (response) {
- onSignInSuccess(response);
- }
- } catch (err: any) {
- onSignInError(err);
- }
- })
- .catch((err) => onSignInError(err));
- };
-
- const handleEmailCodeSignIn = async (response: any) => {
- try {
- if (response) {
- onSignInSuccess(response);
- }
- } catch (err: any) {
- onSignInError(err);
- }
- };
-
- return (
-
-
-
-
-
- {parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
- <>
-
- Sign in to Plane
-
-
- >
- ) : (
-
- )}
-
- {parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
-
- By signing up, you agree to the{" "}
-
- Terms & Conditions
-
-
- ) : null}
-
-
-
- );
-};
+const HomePage = () => ;
export default HomePage;
diff --git a/space/public/site.webmanifest.json b/space/public/site.webmanifest.json
new file mode 100644
index 000000000..4c32ec6e3
--- /dev/null
+++ b/space/public/site.webmanifest.json
@@ -0,0 +1,13 @@
+{
+ "name": "Plane Space",
+ "short_name": "Plane Space",
+ "description": "Plane helps you plan your issues, cycles, and product modules.",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#f9fafb",
+ "theme_color": "#3f76ff",
+ "icons": [
+ { "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
+ { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
+ ]
+}
diff --git a/space/public/user-logged-in.svg b/space/public/user-logged-in.svg
new file mode 100644
index 000000000..e20b49e82
--- /dev/null
+++ b/space/public/user-logged-in.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/.env.example b/web/.env.example
new file mode 100644
index 000000000..50a6209b2
--- /dev/null
+++ b/web/.env.example
@@ -0,0 +1,26 @@
+# Base url for the API requests
+NEXT_PUBLIC_API_BASE_URL=""
+# Extra image domains that need to be added for Next Image
+NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
+# Google Client ID for Google OAuth
+NEXT_PUBLIC_GOOGLE_CLIENTID=""
+# GitHub App ID for GitHub OAuth
+NEXT_PUBLIC_GITHUB_ID=""
+# GitHub App Name for GitHub Integration
+NEXT_PUBLIC_GITHUB_APP_NAME=""
+# Sentry DSN for error monitoring
+NEXT_PUBLIC_SENTRY_DSN=""
+# Enable/Disable OAUTH - default 0 for selfhosted instance
+NEXT_PUBLIC_ENABLE_OAUTH=0
+# Enable/Disable Sentry
+NEXT_PUBLIC_ENABLE_SENTRY=0
+# Enable/Disable session recording
+NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
+# Enable/Disable event tracking
+NEXT_PUBLIC_TRACK_EVENTS=0
+# Slack Client ID for Slack Integration
+NEXT_PUBLIC_SLACK_CLIENT_ID=""
+# For Telemetry, set it to "app.plane.so"
+NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
+# Public boards deploy URL
+NEXT_PUBLIC_DEPLOY_URL=""
\ No newline at end of file
diff --git a/web/components/cycles/gantt-chart/cycle-issues-layout.tsx b/web/components/cycles/gantt-chart/cycle-issues-layout.tsx
index 7741432ce..c18bc9346 100644
--- a/web/components/cycles/gantt-chart/cycle-issues-layout.tsx
+++ b/web/components/cycles/gantt-chart/cycle-issues-layout.tsx
@@ -5,6 +5,7 @@ import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user";
import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view";
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
+import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
@@ -18,6 +19,7 @@ export const CycleIssuesGanttChartView = () => {
const { orderBy } = useIssuesView();
const { user } = useUser();
+ const { projectDetails } = useProjectDetails();
const { ganttIssues, mutateGanttIssues } = useGanttChartCycleIssues(
workspaceSlug as string,
@@ -25,6 +27,8 @@ export const CycleIssuesGanttChartView = () => {
cycleId as string
);
+ const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
+
return (
{
}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
- enableReorder={orderBy === "sort_order"}
+ enableBlockLeftResize={isAllowed}
+ enableBlockRightResize={isAllowed}
+ enableBlockMove={isAllowed}
+ enableReorder={orderBy === "sort_order" && isAllowed}
bottomSpacing
/>
diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx
index a5b576bca..9614ea447 100644
--- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx
+++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx
@@ -8,6 +8,7 @@ import { KeyedMutator } from "swr";
import cyclesService from "services/cycles.service";
// hooks
import useUser from "hooks/use-user";
+import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, IBlockUpdateData } from "components/gantt-chart";
import { CycleGanttBlock, CycleGanttSidebarBlock } from "components/cycles";
@@ -24,6 +25,7 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) =>
const { workspaceSlug } = router.query;
const { user } = useUser();
+ const { projectDetails } = useProjectDetails();
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
if (!workspaceSlug || !user) return;
@@ -71,6 +73,8 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) =>
}))
: [];
+ const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
+
return (
= ({ cycles, mutateCycles }) =>
enableBlockLeftResize={false}
enableBlockRightResize={false}
enableBlockMove={false}
+ enableReorder={isAllowed}
/>
);
diff --git a/web/components/issues/gantt-chart/layout.tsx b/web/components/issues/gantt-chart/layout.tsx
index a42d764d8..39e169a60 100644
--- a/web/components/issues/gantt-chart/layout.tsx
+++ b/web/components/issues/gantt-chart/layout.tsx
@@ -5,6 +5,7 @@ import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user";
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
+import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
@@ -18,12 +19,15 @@ export const IssueGanttChartView = () => {
const { orderBy } = useIssuesView();
const { user } = useUser();
+ const { projectDetails } = useProjectDetails();
const { ganttIssues, mutateGanttIssues } = useGanttChartIssues(
workspaceSlug as string,
projectId as string
);
+ const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
+
return (
{
}
BlockRender={IssueGanttBlock}
SidebarBlockRender={IssueGanttSidebarBlock}
- enableReorder={orderBy === "sort_order"}
+ enableBlockLeftResize={isAllowed}
+ enableBlockRightResize={isAllowed}
+ enableBlockMove={isAllowed}
+ enableReorder={orderBy === "sort_order" && isAllowed}
bottomSpacing
/>
diff --git a/web/components/issues/peek-overview/issue-properties.tsx b/web/components/issues/peek-overview/issue-properties.tsx
index 1f2d618ac..16728b148 100644
--- a/web/components/issues/peek-overview/issue-properties.tsx
+++ b/web/components/issues/peek-overview/issue-properties.tsx
@@ -50,12 +50,9 @@ export const PeekOverviewIssueProperties: React.FC = ({
maxDate?.setDate(maxDate.getDate());
const handleCopyLink = () => {
- const originURL =
- typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
+ const urlToCopy = window.location.href;
- copyTextToClipboard(
- `${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`
- ).then(() => {
+ copyTextToClipboard(urlToCopy).then(() => {
setToastAlert({
type: "success",
title: "Link copied!",
diff --git a/web/components/modules/gantt-chart/module-issues-layout.tsx b/web/components/modules/gantt-chart/module-issues-layout.tsx
index 9c0b05078..c350232e9 100644
--- a/web/components/modules/gantt-chart/module-issues-layout.tsx
+++ b/web/components/modules/gantt-chart/module-issues-layout.tsx
@@ -7,6 +7,7 @@ import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user";
import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view";
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
+import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
@@ -22,6 +23,7 @@ export const ModuleIssuesGanttChartView: FC = ({}) => {
const { orderBy } = useIssuesView();
const { user } = useUser();
+ const { projectDetails } = useProjectDetails();
const { ganttIssues, mutateGanttIssues } = useGanttChartModuleIssues(
workspaceSlug as string,
@@ -29,6 +31,8 @@ export const ModuleIssuesGanttChartView: FC = ({}) => {
moduleId as string
);
+ const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
+
return (
= ({}) => {
}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
- enableReorder={orderBy === "sort_order"}
+ enableBlockLeftResize={isAllowed}
+ enableBlockRightResize={isAllowed}
+ enableBlockMove={isAllowed}
+ enableReorder={orderBy === "sort_order" && isAllowed}
bottomSpacing
/>
diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx
index 70f493dde..08465ffa9 100644
--- a/web/components/modules/gantt-chart/modules-list-layout.tsx
+++ b/web/components/modules/gantt-chart/modules-list-layout.tsx
@@ -1,7 +1,6 @@
import { FC } from "react";
import { useRouter } from "next/router";
-import Link from "next/link";
import { KeyedMutator } from "swr";
@@ -9,6 +8,7 @@ import { KeyedMutator } from "swr";
import modulesService from "services/modules.service";
// hooks
import useUser from "hooks/use-user";
+import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, IBlockUpdateData } from "components/gantt-chart";
import { ModuleGanttBlock, ModuleGanttSidebarBlock } from "components/modules";
@@ -25,6 +25,7 @@ export const ModulesListGanttChartView: FC = ({ modules, mutateModules })
const { workspaceSlug } = router.query;
const { user } = useUser();
+ const { projectDetails } = useProjectDetails();
const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => {
if (!workspaceSlug || !user) return;
@@ -78,6 +79,8 @@ export const ModulesListGanttChartView: FC = ({ modules, mutateModules })
}))
: [];
+ const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
+
return (
= ({ modules, mutateModules })
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
SidebarBlockRender={ModuleGanttSidebarBlock}
BlockRender={ModuleGanttBlock}
+ enableBlockLeftResize={isAllowed}
+ enableBlockRightResize={isAllowed}
+ enableBlockMove={isAllowed}
+ enableReorder={isAllowed}
/>
);
diff --git a/web/components/project/confirm-project-leave-modal.tsx b/web/components/project/confirm-project-leave-modal.tsx
new file mode 100644
index 000000000..429c231d2
--- /dev/null
+++ b/web/components/project/confirm-project-leave-modal.tsx
@@ -0,0 +1,220 @@
+import React from "react";
+// next imports
+import { useRouter } from "next/router";
+// swr
+import { mutate } from "swr";
+// react-hook-form
+import { Controller, useForm } from "react-hook-form";
+// headless ui
+import { Dialog, Transition } from "@headlessui/react";
+// icons
+import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
+// ui
+import { DangerButton, Input, SecondaryButton } from "components/ui";
+// fetch-keys
+import { PROJECTS_LIST } from "constants/fetch-keys";
+// mobx react lite
+import { observer } from "mobx-react-lite";
+// mobx store
+import { useMobxStore } from "lib/mobx/store-provider";
+import { RootStore } from "store/root";
+// hooks
+import useToast from "hooks/use-toast";
+import useUser from "hooks/use-user";
+// types
+import { IProject } from "types";
+
+type FormData = {
+ projectName: string;
+ confirmLeave: string;
+};
+
+const defaultValues: FormData = {
+ projectName: "",
+ confirmLeave: "",
+};
+
+export const ConfirmProjectLeaveModal: React.FC = observer(() => {
+ const router = useRouter();
+ const { workspaceSlug } = router.query;
+
+ const store: RootStore = useMobxStore();
+ const { project } = store;
+
+ const { user } = useUser();
+
+ const { setToastAlert } = useToast();
+
+ const {
+ control,
+ formState: { isSubmitting },
+ handleSubmit,
+ reset,
+ watch,
+ } = useForm({ defaultValues });
+
+ const handleClose = () => {
+ project.handleProjectLeaveModal(null);
+
+ reset({ ...defaultValues });
+ };
+
+ project?.projectLeaveDetails &&
+ console.log("project leave confirmation modal", project?.projectLeaveDetails);
+
+ const onSubmit = async (data: any) => {
+ if (data) {
+ if (data.projectName === project?.projectLeaveDetails?.name) {
+ if (data.confirmLeave === "Leave Project") {
+ return project
+ .leaveProject(
+ project.projectLeaveDetails.workspaceSlug.toString(),
+ project.projectLeaveDetails.id.toString(),
+ user
+ )
+ .then((res) => {
+ mutate(
+ PROJECTS_LIST(project.projectLeaveDetails.workspaceSlug.toString(), {
+ is_favorite: "all",
+ }),
+ (prevData) => prevData?.filter((project: IProject) => project.id !== data.id),
+ false
+ );
+ handleClose();
+ router.push(`/${workspaceSlug}/projects`);
+ })
+ .catch((err) => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Something went wrong please try again later.",
+ });
+ });
+ } else {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Please confirm leaving the project by typing the 'Leave Project'.",
+ });
+ }
+ } else {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Please enter the project name as shown in the description.",
+ });
+ }
+ } else {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Please fill all fields.",
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+});
diff --git a/web/components/project/index.ts b/web/components/project/index.ts
index a2fed74b8..494a04294 100644
--- a/web/components/project/index.ts
+++ b/web/components/project/index.ts
@@ -5,3 +5,4 @@ export * from "./settings-header";
export * from "./single-integration-card";
export * from "./single-project-card";
export * from "./single-sidebar-project";
+export * from "./confirm-project-leave-modal";
diff --git a/web/components/project/publish-project/modal.tsx b/web/components/project/publish-project/modal.tsx
index b22a496f5..173a5242c 100644
--- a/web/components/project/publish-project/modal.tsx
+++ b/web/components/project/publish-project/modal.tsx
@@ -6,7 +6,14 @@ import { Controller, useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui components
-import { ToggleSwitch, PrimaryButton, SecondaryButton, Icon, DangerButton } from "components/ui";
+import {
+ ToggleSwitch,
+ PrimaryButton,
+ SecondaryButton,
+ Icon,
+ DangerButton,
+ Loader,
+} from "components/ui";
import { CustomPopover } from "./popover";
// mobx react lite
import { observer } from "mobx-react-lite";
@@ -146,22 +153,14 @@ export const PublishProjectModal: React.FC = observer(() => {
const projectId = projectPublish.project_id;
return projectPublish
- .createProjectSettingsAsync(
- workspaceSlug.toString(),
- projectId?.toString() ?? "",
- payload,
- user
- )
- .then((response) => {
+ .publishProject(workspaceSlug.toString(), projectId?.toString() ?? "", payload, user)
+ .then((res) => {
mutateProjectDetails();
handleClose();
if (projectId) window.open(`${plane_deploy_url}/${workspaceSlug}/${projectId}`, "_blank");
- return response;
+ return res;
})
- .catch((error) => {
- console.error("error", error);
- return error;
- });
+ .catch((err) => err);
};
const handleUpdatePublishSettings = async (payload: IProjectPublishSettings) => {
@@ -199,7 +198,7 @@ export const PublishProjectModal: React.FC = observer(() => {
setIsUnpublishing(true);
projectPublish
- .deleteProjectSettingsAsync(
+ .unPublishProject(
workspaceSlug.toString(),
projectPublish.project_id as string,
publishId,
@@ -329,7 +328,7 @@ export const PublishProjectModal: React.FC = observer(() => {
{/* heading */}
Publish
- {watch("id") && (
+ {projectPublish.projectPublishSettings !== "not-initialized" && (
handleUnpublishProject(watch("id") ?? "")}
className="!px-2 !py-1.5"
@@ -341,137 +340,145 @@ export const PublishProjectModal: React.FC = observer(() => {
{/* content */}
-
-
-
- {`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`}
-
-
-
-
-
+ {projectPublish.fetchSettingsLoader ? (
+
+
+
+
+
+
+ ) : (
+
+ {watch("id") && (
+ <>
+
+
+ {`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`}
+
+
+
+
+
+
+
+
+
+
This project is live on web
+
+ >
+ )}
- {watch("id") && (
-
-
-
-
-
This project is live on web
-
- )}
-
-
-
-
Views
-
(
- 0
- ? viewOptions
- .filter((v) => value.includes(v.key))
- .map((v) => v.label)
- .join(", ")
- : ``
- }
- placeholder="Select views"
- >
- <>
- {viewOptions.map((option) => (
- {
- const _views =
- value.length > 0
- ? value.includes(option.key)
- ? value.filter((_o: string) => _o !== option.key)
- : [...value, option.key]
- : [option.key];
-
- if (_views.length === 0) return;
-
- onChange(_views);
- checkIfUpdateIsRequired();
- }}
- >
-
{option.label}
+
+
+
Views
+
(
+ 0
+ ? viewOptions
+ .filter((v) => value.includes(v.key))
+ .map((v) => v.label)
+ .join(", ")
+ : ``
+ }
+ placeholder="Select views"
+ >
+ <>
+ {viewOptions.map((option) => (
{
+ const _views =
+ value.length > 0
+ ? value.includes(option.key)
+ ? value.filter((_o: string) => _o !== option.key)
+ : [...value, option.key]
+ : [option.key];
+
+ if (_views.length === 0) return;
+
+ onChange(_views);
+ checkIfUpdateIsRequired();
+ }}
>
- {value.length > 0 && value.includes(option.key) && (
-
- )}
+
{option.label}
+
+ {value.length > 0 && value.includes(option.key) && (
+
+ )}
+
-
- ))}
- >
-
- )}
- />
-
+ ))}
+ >
+
+ )}
+ />
+
+
+
Allow comments
+
(
+ {
+ onChange(val);
+ checkIfUpdateIsRequired();
+ }}
+ size="sm"
+ />
+ )}
+ />
+
+
+
Allow reactions
+
(
+ {
+ onChange(val);
+ checkIfUpdateIsRequired();
+ }}
+ size="sm"
+ />
+ )}
+ />
+
+
+
Allow voting
+
(
+ {
+ onChange(val);
+ checkIfUpdateIsRequired();
+ }}
+ size="sm"
+ />
+ )}
+ />
+
-
-
Allow comments
-
(
- {
- onChange(val);
- checkIfUpdateIsRequired();
- }}
- size="sm"
- />
- )}
- />
-
-
-
Allow reactions
-
(
- {
- onChange(val);
- checkIfUpdateIsRequired();
- }}
- size="sm"
- />
- )}
- />
-
-
-
Allow voting
-
(
- {
- onChange(val);
- checkIfUpdateIsRequired();
- }}
- size="sm"
- />
- )}
- />
-
-
- {/*
+ {/*
Allow issue proposals
= observer(() => {
)}
/>
*/}
+
-
+ )}
{/* modal handlers */}
@@ -490,22 +498,24 @@ export const PublishProjectModal: React.FC
= observer(() => {
Anyone with the link can access
-
-
Cancel
- {watch("id") ? (
- <>
- {isUpdateRequired && (
-
- {isSubmitting ? "Updating..." : "Update settings"}
-
- )}
- >
- ) : (
-
- {isSubmitting ? "Publishing..." : "Publish"}
-
- )}
-
+ {!projectPublish.fetchSettingsLoader && (
+
+
Cancel
+ {watch("id") ? (
+ <>
+ {isUpdateRequired && (
+
+ {isSubmitting ? "Updating..." : "Update settings"}
+
+ )}
+ >
+ ) : (
+
+ {isSubmitting ? "Publishing..." : "Publish"}
+
+ )}
+
+ )}
diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx
index 035a680f2..b8f383f05 100644
--- a/web/components/project/send-project-invitation-modal.tsx
+++ b/web/components/project/send-project-invitation-modal.tsx
@@ -85,7 +85,8 @@ const SendProjectInvitationModal: React.FC
= (props) => {
});
const uninvitedPeople = people?.filter((person) => {
- const isInvited = members?.find((member) => member.display_name === person.member.display_name);
+ const isInvited = members?.find((member) => member.memberId === person.member.id);
+
return !isInvited;
});
@@ -143,7 +144,7 @@ const SendProjectInvitationModal: React.FC = (props) => {
content: (
- {person.member.display_name}
+ {person.member.display_name} ({person.member.first_name + " " + person.member.last_name})
),
}));
diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx
index 0ab8f9bee..a46a97f04 100644
--- a/web/components/project/sidebar-list.tsx
+++ b/web/components/project/sidebar-list.tsx
@@ -35,6 +35,7 @@ export const ProjectSidebarList: FC = () => {
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
const [projectToDelete, setProjectToDelete] = useState(null);
+ const [projectToLeaveId, setProjectToLeaveId] = useState(null);
// router
const [isScrolled, setIsScrolled] = useState(false);
@@ -217,6 +218,7 @@ export const ProjectSidebarList: FC = () => {
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
+ handleProjectLeave={() => setProjectToLeaveId(project.id)}
shortContextMenu
/>
@@ -285,6 +287,7 @@ export const ProjectSidebarList: FC = () => {
provided={provided}
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
+ handleProjectLeave={() => setProjectToLeaveId(project.id)}
handleCopyText={() => handleCopyText(project.id)}
/>
diff --git a/web/components/project/single-sidebar-project.tsx b/web/components/project/single-sidebar-project.tsx
index 6fbdbbaf0..ebc8bc974 100644
--- a/web/components/project/single-sidebar-project.tsx
+++ b/web/components/project/single-sidebar-project.tsx
@@ -44,6 +44,7 @@ type Props = {
snapshot?: DraggableStateSnapshot;
handleDeleteProject: () => void;
handleCopyText: () => void;
+ handleProjectLeave: () => void;
shortContextMenu?: boolean;
};
@@ -80,276 +81,293 @@ const navigation = (workspaceSlug: string, projectId: string) => [
},
];
-export const SingleSidebarProject: React.FC
= observer(
- ({
+export const SingleSidebarProject: React.FC = observer((props) => {
+ const {
project,
sidebarCollapse,
provided,
snapshot,
handleDeleteProject,
handleCopyText,
+ handleProjectLeave,
shortContextMenu = false,
- }) => {
- const store: RootStore = useMobxStore();
- const { projectPublish } = store;
+ } = props;
- const router = useRouter();
- const { workspaceSlug, projectId } = router.query;
+ const store: RootStore = useMobxStore();
+ const { projectPublish, project: projectStore } = store;
- const { setToastAlert } = useToast();
+ const router = useRouter();
+ const { workspaceSlug, projectId } = router.query;
- const isAdmin = project.member_role === 20;
+ const { setToastAlert } = useToast();
- const handleAddToFavorites = () => {
- if (!workspaceSlug) return;
+ const isAdmin = project.member_role === 20;
- mutate(
- PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
- (prevData) =>
- (prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: true } : p)),
- false
- );
+ const isViewerOrGuest = project.member_role === 10 || project.member_role === 5;
- projectService
- .addProjectToFavorites(workspaceSlug as string, {
- project: project.id,
- })
- .catch(() =>
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Couldn't remove the project from favorites. Please try again.",
- })
- );
- };
+ const handleAddToFavorites = () => {
+ if (!workspaceSlug) return;
- const handleRemoveFromFavorites = () => {
- if (!workspaceSlug) return;
+ mutate(
+ PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
+ (prevData) =>
+ (prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: true } : p)),
+ false
+ );
- mutate(
- PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
- (prevData) =>
- (prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: false } : p)),
- false
- );
-
- projectService.removeProjectFromFavorites(workspaceSlug as string, project.id).catch(() =>
+ projectService
+ .addProjectToFavorites(workspaceSlug as string, {
+ project: project.id,
+ })
+ .catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
})
);
- };
+ };
- return (
-
- {({ open }) => (
- <>
-
- {provided && (
-
-
-
- )}
+ const handleRemoveFromFavorites = () => {
+ if (!workspaceSlug) return;
+
+ mutate
(
+ PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
+ (prevData) =>
+ (prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: false } : p)),
+ false
+ );
+
+ projectService.removeProjectFromFavorites(workspaceSlug as string, project.id).catch(() =>
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Couldn't remove the project from favorites. Please try again.",
+ })
+ );
+ };
+
+ return (
+
+ {({ open }) => (
+ <>
+
+ {provided && (
-
+
+
+
+
+ )}
+
+
+
-
- {project.emoji ? (
-
- {renderEmoji(project.emoji)}
-
- ) : project.icon_prop ? (
-
- {renderEmoji(project.icon_prop)}
-
- ) : (
-
- {project?.name.charAt(0)}
-
- )}
+ {project.emoji ? (
+
+ {renderEmoji(project.emoji)}
+
+ ) : project.icon_prop ? (
+
+ {renderEmoji(project.icon_prop)}
+
+ ) : (
+
+ {project?.name.charAt(0)}
+
+ )}
- {!sidebarCollapse && (
-
- {project.name}
-
- )}
-
{!sidebarCollapse && (
-
+
+ {project.name}
+
)}
-
-
+
+ {!sidebarCollapse && (
+
+ )}
+
+
- {!sidebarCollapse && (
-
- {!shortContextMenu && isAdmin && (
-
-
-
- Delete project
-
-
- )}
- {!project.is_favorite && (
-
-
-
- Add to favorites
-
-
- )}
- {project.is_favorite && (
-
-
-
- Remove from favorites
-
-
- )}
-
-
-
- Copy project link
+ {!sidebarCollapse && (
+
+ {!shortContextMenu && isAdmin && (
+
+
+
+ Delete project
+ )}
+ {!project.is_favorite && (
+
+
+
+ Add to favorites
+
+
+ )}
+ {project.is_favorite && (
+
+
+
+ Remove from favorites
+
+
+ )}
+
+
+
+ Copy project link
+
+
- {/* publish project settings */}
- {isAdmin && (
- projectPublish.handleProjectModal(project?.id)}
- >
-
-
-
-
-
{project.is_deployed ? "Publish settings" : "Publish"}
+ {/* publish project settings */}
+ {isAdmin && (
+
projectPublish.handleProjectModal(project?.id)}
+ >
+
+
+
-
- )}
+
{project.is_deployed ? "Publish settings" : "Publish"}
+
+
+ )}
- {project.archive_in > 0 && (
-
- router.push(`/${workspaceSlug}/projects/${project?.id}/archived-issues/`)
- }
- >
-
-
- )}
+ {project.archive_in > 0 && (
- router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)
+ router.push(`/${workspaceSlug}/projects/${project?.id}/archived-issues/`)
}
>
-
-
Settings
+
+
Archived Issues
-
- )}
-
+ )}
+ router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
+ >
+
+
+ Settings
+
+
-
-
- {navigation(workspaceSlug as string, project?.id).map((item) => {
- if (
- (item.name === "Cycles" && !project.cycle_view) ||
- (item.name === "Modules" && !project.module_view) ||
- (item.name === "Views" && !project.issue_views_view) ||
- (item.name === "Pages" && !project.page_view)
- )
- return;
+ {/* leave project */}
+ {isViewerOrGuest && (
+
+ projectStore.handleProjectLeaveModal({
+ id: project?.id,
+ name: project?.name,
+ workspaceSlug: workspaceSlug as string,
+ })
+ }
+ >
+
+
+ Leave Project
+
+
+ )}
+
+ )}
+
- return (
-
-
-
+
+ {navigation(workspaceSlug as string, project?.id).map((item) => {
+ if (
+ (item.name === "Cycles" && !project.cycle_view) ||
+ (item.name === "Modules" && !project.module_view) ||
+ (item.name === "Views" && !project.issue_views_view) ||
+ (item.name === "Pages" && !project.page_view)
+ )
+ return;
+
+ return (
+
+
+
+
-
-
- {!sidebarCollapse && item.name}
-
-
-
-
- );
- })}
-
-
- >
- )}
-
- );
- }
-);
+
+ {!sidebarCollapse && item.name}
+
+
+
+
+ );
+ })}
+
+
+ >
+ )}
+
+ );
+});
diff --git a/web/components/views/gantt-chart.tsx b/web/components/views/gantt-chart.tsx
index 36022f6fa..b25f034cd 100644
--- a/web/components/views/gantt-chart.tsx
+++ b/web/components/views/gantt-chart.tsx
@@ -6,6 +6,7 @@ import { useRouter } from "next/router";
import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view";
import useUser from "hooks/use-user";
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
+import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
@@ -19,6 +20,7 @@ export const ViewIssuesGanttChartView: FC = ({}) => {
const { workspaceSlug, projectId, viewId } = router.query;
const { user } = useUser();
+ const { projectDetails } = useProjectDetails();
const { ganttIssues, mutateGanttIssues } = useGanttChartViewIssues(
workspaceSlug as string,
@@ -26,6 +28,8 @@ export const ViewIssuesGanttChartView: FC = ({}) => {
viewId as string
);
+ const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
+
return (
= ({}) => {
}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
+ enableBlockLeftResize={isAllowed}
+ enableBlockRightResize={isAllowed}
+ enableBlockMove={isAllowed}
+ enableReorder={isAllowed}
/>
);
diff --git a/web/layouts/app-layout/app-sidebar.tsx b/web/layouts/app-layout/app-sidebar.tsx
index 9290c00c6..03ac72387 100644
--- a/web/layouts/app-layout/app-sidebar.tsx
+++ b/web/layouts/app-layout/app-sidebar.tsx
@@ -9,6 +9,7 @@ import {
} from "components/workspace";
import { ProjectSidebarList } from "components/project";
import { PublishProjectModal } from "components/project/publish-project/modal";
+import { ConfirmProjectLeaveModal } from "components/project/confirm-project-leave-modal";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
@@ -38,7 +39,10 @@ const Sidebar: React.FC = observer(({ toggleSidebar, setToggleSide
+ {/* publish project modal */}
+ {/* project leave modal */}
+
);
});
diff --git a/web/services/project.service.ts b/web/services/project.service.ts
index 961333bee..0c2712c56 100644
--- a/web/services/project.service.ts
+++ b/web/services/project.service.ts
@@ -21,7 +21,7 @@ const { NEXT_PUBLIC_API_BASE_URL } = process.env;
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
-class ProjectServices extends APIService {
+export class ProjectServices extends APIService {
constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
@@ -142,6 +142,30 @@ class ProjectServices extends APIService {
});
}
+ async leaveProject(
+ workspaceSlug: string,
+ projectId: string,
+ user: ICurrentUserResponse
+ ): Promise {
+ return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`)
+ .then((response) => {
+ if (trackEvent)
+ trackEventServices.trackProjectEvent(
+ "PROJECT_MEMBER_LEAVE",
+ {
+ workspaceSlug,
+ projectId,
+ ...response?.data,
+ },
+ user
+ );
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
async joinProjects(data: any): Promise {
return this.post("/api/users/me/invitations/projects/", data)
.then((response) => response?.data)
diff --git a/web/services/track-event.service.ts b/web/services/track-event.service.ts
index f55a6f366..c59242f50 100644
--- a/web/services/track-event.service.ts
+++ b/web/services/track-event.service.ts
@@ -35,7 +35,8 @@ type ProjectEventType =
| "CREATE_PROJECT"
| "UPDATE_PROJECT"
| "DELETE_PROJECT"
- | "PROJECT_MEMBER_INVITE";
+ | "PROJECT_MEMBER_INVITE"
+ | "PROJECT_MEMBER_LEAVE";
type IssueEventType = "ISSUE_CREATE" | "ISSUE_UPDATE" | "ISSUE_DELETE";
@@ -163,7 +164,11 @@ class TrackEventServices extends APIService {
user: ICurrentUserResponse | undefined
): Promise {
let payload: any;
- if (eventName !== "DELETE_PROJECT" && eventName !== "PROJECT_MEMBER_INVITE")
+ if (
+ eventName !== "DELETE_PROJECT" &&
+ eventName !== "PROJECT_MEMBER_INVITE" &&
+ eventName !== "PROJECT_MEMBER_LEAVE"
+ )
payload = {
workspaceId: data?.workspace_detail?.id,
workspaceName: data?.workspace_detail?.name,
diff --git a/web/store/project-publish.tsx b/web/store/project-publish.tsx
index ffc45f546..b8c6f0dbe 100644
--- a/web/store/project-publish.tsx
+++ b/web/store/project-publish.tsx
@@ -21,7 +21,8 @@ export interface IProjectPublishSettings {
}
export interface IProjectPublishStore {
- loader: boolean;
+ generalLoader: boolean;
+ fetchSettingsLoader: boolean;
error: any | null;
projectPublishModal: boolean;
@@ -35,7 +36,7 @@ export interface IProjectPublishStore {
project_slug: string,
user: any
) => Promise;
- createProjectSettingsAsync: (
+ publishProject: (
workspace_slug: string,
project_slug: string,
data: IProjectPublishSettings,
@@ -48,7 +49,7 @@ export interface IProjectPublishStore {
data: IProjectPublishSettings,
user: any
) => Promise;
- deleteProjectSettingsAsync: (
+ unPublishProject: (
workspace_slug: string,
project_slug: string,
project_publish_id: string,
@@ -57,7 +58,8 @@ export interface IProjectPublishStore {
}
class ProjectPublishStore implements IProjectPublishStore {
- loader: boolean = false;
+ generalLoader: boolean = false;
+ fetchSettingsLoader: boolean = false;
error: any | null = null;
projectPublishModal: boolean = false;
@@ -72,7 +74,8 @@ class ProjectPublishStore implements IProjectPublishStore {
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observable
- loader: observable,
+ generalLoader: observable,
+ fetchSettingsLoader: observable,
error: observable,
projectPublishModal: observable,
@@ -80,6 +83,10 @@ class ProjectPublishStore implements IProjectPublishStore {
projectPublishSettings: observable.ref,
// action
handleProjectModal: action,
+ getProjectSettingsAsync: action,
+ publishProject: action,
+ updateProjectSettingsAsync: action,
+ unPublishProject: action,
// computed
});
@@ -100,7 +107,7 @@ class ProjectPublishStore implements IProjectPublishStore {
getProjectSettingsAsync = async (workspace_slug: string, project_slug: string, user: any) => {
try {
- this.loader = true;
+ this.fetchSettingsLoader = true;
this.error = null;
const response = await this.projectPublishService.getProjectSettingsAsync(
@@ -128,30 +135,30 @@ class ProjectPublishStore implements IProjectPublishStore {
runInAction(() => {
this.projectPublishSettings = _projectPublishSettings;
- this.loader = false;
+ this.fetchSettingsLoader = false;
this.error = null;
});
} else {
this.projectPublishSettings = "not-initialized";
- this.loader = false;
+ this.fetchSettingsLoader = false;
this.error = null;
}
return response;
} catch (error) {
- this.loader = false;
+ this.fetchSettingsLoader = false;
this.error = error;
return error;
}
};
- createProjectSettingsAsync = async (
+ publishProject = async (
workspace_slug: string,
project_slug: string,
data: IProjectPublishSettings,
user: any
) => {
try {
- this.loader = true;
+ this.generalLoader = true;
this.error = null;
const response = await this.projectPublishService.createProjectSettingsAsync(
@@ -174,14 +181,14 @@ class ProjectPublishStore implements IProjectPublishStore {
runInAction(() => {
this.projectPublishSettings = _projectPublishSettings;
- this.loader = false;
+ this.generalLoader = false;
this.error = null;
});
return response;
}
} catch (error) {
- this.loader = false;
+ this.generalLoader = false;
this.error = error;
return error;
}
@@ -195,7 +202,7 @@ class ProjectPublishStore implements IProjectPublishStore {
user: any
) => {
try {
- this.loader = true;
+ this.generalLoader = true;
this.error = null;
const response = await this.projectPublishService.updateProjectSettingsAsync(
@@ -219,27 +226,27 @@ class ProjectPublishStore implements IProjectPublishStore {
runInAction(() => {
this.projectPublishSettings = _projectPublishSettings;
- this.loader = false;
+ this.generalLoader = false;
this.error = null;
});
return response;
}
} catch (error) {
- this.loader = false;
+ this.generalLoader = false;
this.error = error;
return error;
}
};
- deleteProjectSettingsAsync = async (
+ unPublishProject = async (
workspace_slug: string,
project_slug: string,
project_publish_id: string,
user: any
) => {
try {
- this.loader = true;
+ this.generalLoader = true;
this.error = null;
const response = await this.projectPublishService.deleteProjectSettingsAsync(
@@ -251,13 +258,13 @@ class ProjectPublishStore implements IProjectPublishStore {
runInAction(() => {
this.projectPublishSettings = "not-initialized";
- this.loader = false;
+ this.generalLoader = false;
this.error = null;
});
return response;
} catch (error) {
- this.loader = false;
+ this.generalLoader = false;
this.error = error;
return error;
}
diff --git a/web/store/project.ts b/web/store/project.ts
new file mode 100644
index 000000000..0fe842dad
--- /dev/null
+++ b/web/store/project.ts
@@ -0,0 +1,86 @@
+import { observable, action, computed, makeObservable, runInAction } from "mobx";
+// types
+import { RootStore } from "./root";
+// services
+import { ProjectServices } from "services/project.service";
+
+export interface IProject {
+ id: string;
+ name: string;
+ workspaceSlug: string;
+}
+
+export interface IProjectStore {
+ loader: boolean;
+ error: any | null;
+
+ projectLeaveModal: boolean;
+ projectLeaveDetails: IProject | any;
+
+ handleProjectLeaveModal: (project: IProject | null) => void;
+
+ leaveProject: (workspace_slug: string, project_slug: string, user: any) => Promise;
+}
+
+class ProjectStore implements IProjectStore {
+ loader: boolean = false;
+ error: any | null = null;
+
+ projectLeaveModal: boolean = false;
+ projectLeaveDetails: IProject | null = null;
+
+ // root store
+ rootStore;
+ // service
+ projectService;
+
+ constructor(_rootStore: RootStore) {
+ makeObservable(this, {
+ // observable
+ loader: observable,
+ error: observable,
+
+ projectLeaveModal: observable,
+ projectLeaveDetails: observable.ref,
+ // action
+ handleProjectLeaveModal: action,
+ leaveProject: action,
+ // computed
+ });
+
+ this.rootStore = _rootStore;
+ this.projectService = new ProjectServices();
+ }
+
+ handleProjectLeaveModal = (project: IProject | null = null) => {
+ if (project && project?.id) {
+ this.projectLeaveModal = !this.projectLeaveModal;
+ this.projectLeaveDetails = project;
+ } else {
+ this.projectLeaveModal = !this.projectLeaveModal;
+ this.projectLeaveDetails = null;
+ }
+ };
+
+ leaveProject = async (workspace_slug: string, project_slug: string, user: any) => {
+ try {
+ this.loader = true;
+ this.error = null;
+
+ const response = await this.projectService.leaveProject(workspace_slug, project_slug, user);
+
+ runInAction(() => {
+ this.loader = false;
+ this.error = null;
+ });
+
+ return response;
+ } catch (error) {
+ this.loader = false;
+ this.error = error;
+ return error;
+ }
+ };
+}
+
+export default ProjectStore;
diff --git a/web/store/root.ts b/web/store/root.ts
index 2e7a0f242..2e81922d2 100644
--- a/web/store/root.ts
+++ b/web/store/root.ts
@@ -3,15 +3,17 @@ import { enableStaticRendering } from "mobx-react-lite";
// store imports
import UserStore from "./user";
import ThemeStore from "./theme";
-import IssuesStore from "./issues";
+import ProjectStore, { IProjectStore } from "./project";
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
import KanbanStore from "./kanban";
+import IssuesStore from "./issues";
enableStaticRendering(typeof window === "undefined");
export class RootStore {
user;
theme;
+ project: IProjectStore;
projectPublish: IProjectPublishStore;
issues: IssuesStore;
kanban: KanbanStore;
@@ -19,6 +21,7 @@ export class RootStore {
constructor() {
this.user = new UserStore(this);
this.theme = new ThemeStore(this);
+ this.project = new ProjectStore(this);
this.projectPublish = new ProjectPublishStore(this);
this.issues = new IssuesStore(this);
this.kanban = new KanbanStore(this);