Merge branch 'develop' of github.com:makeplane/plane into fix/page_structuring

This commit is contained in:
NarayanBavisetti 2023-10-09 11:16:33 +05:30
commit 1039337c45
61 changed files with 985 additions and 519 deletions

View File

@ -72,6 +72,6 @@ jobs:
gh pr create \ gh pr create \
--base $TARGET_BRANCH \ --base $TARGET_BRANCH \
--head $SOURCE_BRANCH \ --head $SOURCE_BRANCH \
--title "$PR_TITLE_CLEANED" \ --title "[SYNC] $PR_TITLE_CLEANED" \
--body "$PR_BODY_CONTENT" \ --body "$PR_BODY_CONTENT" \
--repo $TARGET_REPO --repo $TARGET_REPO

View File

@ -60,5 +60,13 @@ DEFAULT_PASSWORD="password123"
# SignUps # SignUps
ENABLE_SIGNUP="1" ENABLE_SIGNUP="1"
# Enable Email/Password Signup
ENABLE_EMAIL_PASSWORD="1"
# Enable Magic link Login
ENABLE_MAGIC_LINK_LOGIN="0"
# Email redirections and minio domain settings # Email redirections and minio domain settings
WEB_URL="http://localhost" WEB_URL="http://localhost"

View File

@ -70,6 +70,7 @@ from plane.api.views import (
ProjectIdentifierEndpoint, ProjectIdentifierEndpoint,
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
LeaveProjectEndpoint, LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint,
## End Projects ## End Projects
# Issues # Issues
IssueViewSet, IssueViewSet,
@ -150,12 +151,11 @@ from plane.api.views import (
GlobalSearchEndpoint, GlobalSearchEndpoint,
IssueSearchEndpoint, IssueSearchEndpoint,
## End Search ## End Search
# Gpt # External
GPTIntegrationEndpoint, GPTIntegrationEndpoint,
## End Gpt
# Release Notes
ReleaseNotesEndpoint, ReleaseNotesEndpoint,
## End Release Notes UnsplashEndpoint,
## End External
# Inbox # Inbox
InboxViewSet, InboxViewSet,
InboxIssueViewSet, InboxIssueViewSet,
@ -186,6 +186,9 @@ from plane.api.views import (
## Exporter ## Exporter
ExportIssuesEndpoint, ExportIssuesEndpoint,
## End Exporter ## End Exporter
# Configuration
ConfigurationEndpoint,
## End Configuration
) )
@ -573,6 +576,11 @@ urlpatterns = [
LeaveProjectEndpoint.as_view(), LeaveProjectEndpoint.as_view(),
name="project", name="project",
), ),
path(
"project-covers/",
ProjectPublicCoverImagesEndpoint.as_view(),
name="project-covers",
),
# End Projects # End Projects
# States # States
path( path(
@ -1446,20 +1454,23 @@ urlpatterns = [
name="project-issue-search", name="project-issue-search",
), ),
## End Search ## End Search
# Gpt # External
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/", "workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(), GPTIntegrationEndpoint.as_view(),
name="importer", name="importer",
), ),
## End Gpt
# Release Notes
path( path(
"release-notes/", "release-notes/",
ReleaseNotesEndpoint.as_view(), ReleaseNotesEndpoint.as_view(),
name="release-notes", name="release-notes",
), ),
## End Release Notes path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="release-notes",
),
## End External
# Inbox # Inbox
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/", "workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
@ -1728,4 +1739,11 @@ urlpatterns = [
name="workspace-project-boards", name="workspace-project-boards",
), ),
## End Public Boards ## End Public Boards
# Configuration
path(
"configs/",
ConfigurationEndpoint.as_view(),
name="configuration",
),
## End Configuration
] ]

View File

@ -17,6 +17,7 @@ from .project import (
ProjectMemberEndpoint, ProjectMemberEndpoint,
WorkspaceProjectDeployBoardEndpoint, WorkspaceProjectDeployBoardEndpoint,
LeaveProjectEndpoint, LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint,
) )
from .user import ( from .user import (
UserEndpoint, UserEndpoint,
@ -147,16 +148,13 @@ from .page import (
from .search import GlobalSearchEndpoint, IssueSearchEndpoint from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .gpt import GPTIntegrationEndpoint from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
from .estimate import ( from .estimate import (
ProjectEstimatePointEndpoint, ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint, BulkEstimatePointEndpoint,
) )
from .release import ReleaseNotesEndpoint
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
from .analytic import ( from .analytic import (
@ -169,4 +167,6 @@ from .analytic import (
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
from .exporter import ExportIssuesEndpoint from .exporter import ExportIssuesEndpoint
from .config import ConfigurationEndpoint

View File

@ -0,0 +1,40 @@
# Python imports
import os
# Django imports
from django.conf import settings
# Third party imports
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
class ConfigurationEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request):
try:
data = {}
data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None)
data["github"] = os.environ.get("GITHUB_CLIENT_ID", None)
data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None)
data["magic_login"] = (
bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD)
) and os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0") == "1"
data["email_password_login"] = (
os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
)
return Response(data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -2,9 +2,10 @@
import requests import requests
# Third party imports # Third party imports
import openai
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
import openai from rest_framework.permissions import AllowAny
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Django imports # Django imports
@ -15,6 +16,7 @@ from .base import BaseAPIView
from plane.api.permissions import ProjectEntityPermission from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Workspace, Project from plane.db.models import Workspace, Project
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
from plane.utils.integrations.github import get_release_notes
class GPTIntegrationEndpoint(BaseAPIView): class GPTIntegrationEndpoint(BaseAPIView):
@ -73,3 +75,44 @@ class GPTIntegrationEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
class ReleaseNotesEndpoint(BaseAPIView):
def get(self, request):
try:
release_notes = get_release_notes()
return Response(release_notes, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UnsplashEndpoint(BaseAPIView):
def get(self, request):
try:
query = request.GET.get("query", False)
page = request.GET.get("page", 1)
per_page = request.GET.get("per_page", 20)
url = (
f"https://api.unsplash.com/search/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}"
if query
else f"https://api.unsplash.com/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
)
headers = {
"Content-Type": "application/json",
}
resp = requests.get(url=url, headers=headers)
return Response(resp.json(), status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -1,5 +1,6 @@
# Python imports # Python imports
import jwt import jwt
import boto3
from datetime import datetime from datetime import datetime
# Django imports # Django imports
@ -617,7 +618,8 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectMember.DoesNotExist: except ProjectMember.DoesNotExist:
return Response( return Response(
{"error": "Project Member does not exist"}, status=status.HTTP_400_BAD_REQUEST {"error": "Project Member does not exist"},
status=status.HTTP_400_BAD_REQUEST,
) )
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)
@ -1209,3 +1211,38 @@ class LeaveProjectEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
class ProjectPublicCoverImagesEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request):
try:
files = []
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
params = {
"Bucket": settings.AWS_S3_BUCKET_NAME,
"Prefix": "static/project-cover/",
}
response = s3.list_objects_v2(**params)
# Extracting file keys from the response
if "Contents" in response:
for content in response["Contents"]:
if not content["Key"].endswith(
"/"
): # This line ensures we're only getting files, not "sub-folders"
files.append(
f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
)
return Response(files, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response([], status=status.HTTP_200_OK)

View File

@ -1,21 +0,0 @@
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
from plane.utils.integrations.github import get_release_notes
class ReleaseNotesEndpoint(BaseAPIView):
def get(self, request):
try:
release_notes = get_release_notes()
return Response(release_notes, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -114,3 +114,6 @@ CELERY_BROKER_URL = os.environ.get("REDIS_URL")
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
# Unsplash Access key
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")

View File

@ -275,3 +275,7 @@ ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False) SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
SCOUT_KEY = os.environ.get("SCOUT_KEY", "") SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
SCOUT_NAME = "Plane" SCOUT_NAME = "Plane"
# Unsplash Access key
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")

View File

@ -126,3 +126,4 @@ ANALYTICS_BASE_API = False
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1") OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo") GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")

View File

@ -218,3 +218,7 @@ CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
# Unsplash Access key
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")

View File

@ -1,5 +1,6 @@
{ {
"repository": "https://github.com/makeplane/plane.git", "repository": "https://github.com/makeplane/plane.git",
"version": "0.13.2",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"workspaces": [ "workspaces": [

View File

@ -1,6 +1,6 @@
{ {
"name": "eslint-config-custom", "name": "eslint-config-custom",
"version": "0.0.0", "version": "0.13.2",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "tailwind-config-custom", "name": "tailwind-config-custom",
"version": "0.0.1", "version": "0.13.2",
"description": "common tailwind configuration across monorepo", "description": "common tailwind configuration across monorepo",
"main": "index.js", "main": "index.js",
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "tsconfig", "name": "tsconfig",
"version": "0.0.0", "version": "0.13.2",
"private": true, "private": true,
"files": [ "files": [
"base.json", "base.json",

View File

@ -1,6 +1,6 @@
{ {
"name": "ui", "name": "ui",
"version": "0.0.0", "version": "0.13.2",
"main": "./index.tsx", "main": "./index.tsx",
"types": "./index.tsx", "types": "./index.tsx",
"license": "MIT", "license": "MIT",

View File

@ -10,9 +10,12 @@ import githubWhiteImage from "public/logos/github-white.svg";
export interface GithubLoginButtonProps { export interface GithubLoginButtonProps {
handleSignIn: React.Dispatch<string>; handleSignIn: React.Dispatch<string>;
clientId: string;
} }
export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => { export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
const { handleSignIn, clientId } = props;
// states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
const [gitCode, setGitCode] = useState<null | string>(null); const [gitCode, setGitCode] = useState<null | string>(null);
@ -38,7 +41,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn })
<div className="w-full flex justify-center items-center"> <div className="w-full flex justify-center items-center">
<Link <Link
className="w-full" className="w-full"
href={`https://github.com/login/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`} href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
> >
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]"> <button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
<Image <Image

View File

@ -1,22 +1,23 @@
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react"; import { FC, useEffect, useRef, useCallback, useState } from "react";
import Script from "next/script"; import Script from "next/script";
export interface IGoogleLoginButton { export interface IGoogleLoginButton {
text?: string; clientId: string;
handleSignIn: React.Dispatch<any>; handleSignIn: React.Dispatch<any>;
styles?: CSSProperties;
} }
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => { export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
const { handleSignIn, clientId } = props;
// refs
const googleSignInButton = useRef<HTMLDivElement>(null); const googleSignInButton = useRef<HTMLDivElement>(null);
// states
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false); const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
const loadScript = useCallback(() => { const loadScript = useCallback(() => {
if (!googleSignInButton.current || gsiScriptLoaded) return; if (!googleSignInButton.current || gsiScriptLoaded) return;
(window as any)?.google?.accounts.id.initialize({ (window as any)?.google?.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "", client_id: clientId,
callback: handleSignIn, callback: handleSignIn,
}); });

View File

@ -1,26 +1,30 @@
import React, { useEffect } from "react"; import React from "react";
import useSWR from "swr";
import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx // mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import authenticationService from "services/authentication.service"; import authenticationService from "services/authentication.service";
import { AppConfigService } from "services/app-config.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts"; import { EmailPasswordForm, GoogleLoginButton, EmailCodeForm } from "components/accounts";
// images // images
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : ""; const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
const appConfig = new AppConfigService();
export const SignInView = observer(() => { export const SignInView = observer(() => {
const { user: userStore } = useMobxStore(); const { user: userStore } = useMobxStore();
// router
const router = useRouter(); const router = useRouter();
const { next_path } = router.query as { next_path: string };
// toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// fetch app config
const { data } = useSWR("APP_CONFIG", () => appConfig.envConfig());
const onSignInError = (error: any) => { const onSignInError = (error: any) => {
setToastAlert({ setToastAlert({
@ -31,17 +35,17 @@ export const SignInView = observer(() => {
}; };
const onSignInSuccess = (response: any) => { const onSignInSuccess = (response: any) => {
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/login";
userStore.setCurrentUser(response?.user); userStore.setCurrentUser(response?.user);
if (!isOnboarded) { const isOnboard = response?.user?.onboarding_step?.profile_complete || false;
router.push(`/onboarding?next_path=${nextPath}`);
return; if (isOnboard) {
if (next_path) router.push(next_path);
else router.push("/login");
} else {
if (next_path) router.push(`/onboarding?next_path=${next_path}`);
else router.push("/onboarding");
} }
router.push((nextPath ?? "/login").toString());
}; };
const handleGoogleSignIn = async ({ clientId, credential }: any) => { const handleGoogleSignIn = async ({ clientId, credential }: any) => {
@ -63,24 +67,6 @@ export const SignInView = observer(() => {
} }
}; };
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) => { const handlePasswordSignIn = async (formData: any) => {
await authenticationService await authenticationService
.emailLogin(formData) .emailLogin(formData)
@ -118,38 +104,32 @@ export const SignInView = observer(() => {
</div> </div>
<div className="grid place-items-center h-full overflow-y-auto py-5 px-7"> <div className="grid place-items-center h-full overflow-y-auto py-5 px-7">
<div> <div>
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? ( <h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">Sign in to Plane</h1>
<> {data?.email_password_login && <EmailPasswordForm onSubmit={handlePasswordSignIn} />}
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
Sign in to Plane {data?.magic_login && (
</h1> <div className="flex flex-col divide-y divide-custom-border-200">
<div className="flex flex-col divide-y divide-custom-border-200"> <div className="pb-7">
<div className="pb-7"> <EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
</div>
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
<GoogleLoginButton handleSignIn={handleGoogleSignIn} />
{/* <GithubLoginButton handleSignIn={handleGitHubSignIn} /> */}
</div>
</div> </div>
</> </div>
) : (
<EmailPasswordForm onSubmit={handlePasswordSignIn} />
)} )}
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? ( <div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
<p className="pt-16 text-custom-text-200 text-sm text-center"> {data?.google && <GoogleLoginButton clientId={data.google} handleSignIn={handleGoogleSignIn} />}
By signing up, you agree to the{" "} </div>
<a
href="https://plane.so/terms-and-conditions" <p className="pt-16 text-custom-text-200 text-sm text-center">
target="_blank" By signing up, you agree to the{" "}
rel="noopener noreferrer" <a
className="font-medium underline" href="https://plane.so/terms-and-conditions"
> target="_blank"
Terms & Conditions rel="noopener noreferrer"
</a> className="font-medium underline"
</p> >
) : null} Terms & Conditions
</a>
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -44,19 +44,43 @@ const IssueNavbar = observer(() => {
}, [projectStore, workspace_slug, project_slug]); }, [projectStore, workspace_slug, project_slug]);
useEffect(() => { useEffect(() => {
if (workspace_slug && project_slug) { if (workspace_slug && project_slug && projectStore?.deploySettings) {
if (!board) { const viewsAcceptable: string[] = [];
router.push({ let currentBoard: string | null = null;
pathname: `/${workspace_slug}/${project_slug}`,
query: { if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("list");
board: "list", if (projectStore?.deploySettings?.views?.kanban) viewsAcceptable.push("kanban");
}, if (projectStore?.deploySettings?.views?.calendar) viewsAcceptable.push("calendar");
}); if (projectStore?.deploySettings?.views?.gantt) viewsAcceptable.push("gantt");
return projectStore.setActiveBoard("list"); if (projectStore?.deploySettings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet");
if (board) {
if (viewsAcceptable.includes(board.toString())) {
currentBoard = board.toString();
} else {
if (viewsAcceptable && viewsAcceptable.length > 0) {
currentBoard = viewsAcceptable[0];
}
}
} else {
if (viewsAcceptable && viewsAcceptable.length > 0) {
currentBoard = viewsAcceptable[0];
}
}
if (currentBoard) {
if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) {
projectStore.setActiveBoard(currentBoard);
router.push({
pathname: `/${workspace_slug}/${project_slug}`,
query: {
board: currentBoard,
},
});
}
} }
projectStore.setActiveBoard(board.toString());
} }
}, [board, workspace_slug, project_slug]); }, [board, workspace_slug, project_slug, router, projectStore, projectStore?.deploySettings]);
return ( return (
<div className="px-5 relative w-full flex items-center gap-4"> <div className="px-5 relative w-full flex items-center gap-4">
@ -105,7 +129,7 @@ const IssueNavbar = observer(() => {
</div> </div>
) : ( ) : (
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Link href={`/?next_path=${router.asPath}`}> <Link href={`/login/?next_path=${router.asPath}`}>
<a> <a>
<PrimaryButton className="flex-shrink-0" outline> <PrimaryButton className="flex-shrink-0" outline>
Sign in Sign in

View File

@ -7,7 +7,13 @@ import { SignInView, UserLoggedIn } from "components/accounts";
export const LoginView = observer(() => { export const LoginView = observer(() => {
const { user: userStore } = useMobxStore(); const { user: userStore } = useMobxStore();
if (!userStore.currentUser) return <SignInView />; return (
<>
return <UserLoggedIn />; {userStore?.loader ? (
<div className="relative w-screen h-screen flex justify-center items-center">Loading</div>
) : (
<>{userStore.currentUser ? <UserLoggedIn /> : <SignInView />}</>
)}
</>
);
}); });

View File

@ -3,12 +3,14 @@
import { useEffect } from "react"; import { useEffect } from "react";
// next imports // next imports
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// js cookie
import Cookie from "js-cookie";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
const MobxStoreInit = () => { const MobxStoreInit = () => {
const store: RootStore = useMobxStore(); const { user: userStore }: RootStore = useMobxStore();
const router = useRouter(); const router = useRouter();
const { states, labels, priorities } = router.query as { states: string[]; labels: string[]; priorities: string[] }; const { states, labels, priorities } = router.query as { states: string[]; labels: string[]; priorities: string[] };
@ -19,6 +21,11 @@ const MobxStoreInit = () => {
// store.issue.userSelectedStates = states || []; // store.issue.userSelectedStates = states || [];
// }, [store.issue]); // }, [store.issue]);
useEffect(() => {
const authToken = Cookie.get("accessToken") || null;
if (authToken) userStore.fetchCurrentUser();
}, [userStore]);
return <></>; return <></>;
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "space", "name": "space",
"version": "0.0.1", "version": "0.13.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 4000", "dev": "next dev -p 4000",

19
space/pages/index.tsx Normal file
View File

@ -0,0 +1,19 @@
import { useEffect } from "react";
// next
import { NextPage } from "next";
import { useRouter } from "next/router";
const Index: NextPage = () => {
const router = useRouter();
const { next_path } = router.query as { next_path: string };
useEffect(() => {
if (next_path) router.push(`/login?next_path=${next_path}`);
else router.push(`/login`);
}, [router, next_path]);
return null;
};
export default Index;

View File

@ -5,4 +5,4 @@ import { LoginView } from "components/views";
const LoginPage = () => <LoginView />; const LoginPage = () => <LoginView />;
export default LoginPage; export default LoginPage;

View File

@ -0,0 +1,30 @@
// services
import APIService from "services/api.service";
// helper
import { API_BASE_URL } from "helpers/common.helper";
export interface IEnvConfig {
github: string;
google: string;
github_app_name: string | null;
email_password_login: boolean;
magic_login: boolean;
}
export class AppConfigService extends APIService {
constructor() {
super(API_BASE_URL);
}
async envConfig(): Promise<IEnvConfig> {
return this.get("/api/configs/", {
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}

View File

@ -74,24 +74,6 @@ class FileServices extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async getUnsplashImages(page: number = 1, query?: string): Promise<UnSplashImage[]> {
const url = "/api/unsplash";
return this.request({
method: "get",
url,
params: {
page,
per_page: 20,
query,
},
})
.then((response) => response?.data?.results ?? response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
} }
const fileServices = new FileServices(); const fileServices = new FileServices();

View File

@ -7,12 +7,17 @@ import { ActorDetail } from "types/issue";
import { IUser } from "types/user"; import { IUser } from "types/user";
export interface IUserStore { export interface IUserStore {
loader: boolean;
error: any | null;
currentUser: any | null; currentUser: any | null;
fetchCurrentUser: () => void; fetchCurrentUser: () => void;
currentActor: () => any; currentActor: () => any;
} }
class UserStore implements IUserStore { class UserStore implements IUserStore {
loader: boolean = false;
error: any | null = null;
currentUser: IUser | null = null; currentUser: IUser | null = null;
// root store // root store
rootStore; rootStore;
@ -22,6 +27,9 @@ class UserStore implements IUserStore {
constructor(_rootStore: any) { constructor(_rootStore: any) {
makeObservable(this, { makeObservable(this, {
// observable // observable
loader: observable.ref,
error: observable.ref,
currentUser: observable.ref, currentUser: observable.ref,
// actions // actions
setCurrentUser: action, setCurrentUser: action,
@ -73,14 +81,19 @@ class UserStore implements IUserStore {
fetchCurrentUser = async () => { fetchCurrentUser = async () => {
try { try {
this.loader = true;
this.error = null;
const response = await this.userService.currentUser(); const response = await this.userService.currentUser();
if (response) { if (response) {
runInAction(() => { runInAction(() => {
this.loader = false;
this.currentUser = response; this.currentUser = response;
}); });
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch current user", error); console.error("Failed to fetch current user", error);
this.loader = false;
this.error = error;
} }
}; };
} }

View File

@ -1,8 +1,6 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"globalEnv": [ "globalEnv": [
"NEXT_PUBLIC_GITHUB_ID",
"NEXT_PUBLIC_GOOGLE_CLIENTID",
"NEXT_PUBLIC_API_BASE_URL", "NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_DEPLOY_URL", "NEXT_PUBLIC_DEPLOY_URL",
"API_BASE_URL", "API_BASE_URL",
@ -12,8 +10,6 @@
"NEXT_PUBLIC_GITHUB_APP_NAME", "NEXT_PUBLIC_GITHUB_APP_NAME",
"NEXT_PUBLIC_ENABLE_SENTRY", "NEXT_PUBLIC_ENABLE_SENTRY",
"NEXT_PUBLIC_ENABLE_OAUTH", "NEXT_PUBLIC_ENABLE_OAUTH",
"NEXT_PUBLIC_UNSPLASH_ACCESS",
"NEXT_PUBLIC_UNSPLASH_ENABLED",
"NEXT_PUBLIC_TRACK_EVENTS", "NEXT_PUBLIC_TRACK_EVENTS",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN", "NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_CRISP_ID", "NEXT_PUBLIC_CRISP_ID",

View File

@ -1,12 +1,5 @@
import React, { useState } from "react"; import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// react hook form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// components
import { EmailResetPasswordForm } from "components/account";
// ui // ui
import { Input, PrimaryButton } from "components/ui"; import { Input, PrimaryButton } from "components/ui";
// types // types
@ -18,14 +11,12 @@ type EmailPasswordFormValues = {
type Props = { type Props = {
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>; onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
setIsResettingPassword: (value: boolean) => void;
}; };
export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => { export const EmailPasswordForm: React.FC<Props> = (props) => {
const [isResettingPassword, setIsResettingPassword] = useState(false); const { onSubmit, setIsResettingPassword } = props;
// form info
const router = useRouter();
const isSignUpPage = router.pathname === "/sign-up";
const { const {
register, register,
handleSubmit, handleSubmit,
@ -42,94 +33,62 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
return ( return (
<> <>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100"> <form
{isResettingPassword className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
? "Reset your password" onSubmit={handleSubmit(onSubmit)}
: isSignUpPage >
? "Sign up on Plane" <div className="space-y-1">
: "Sign in to Plane"} <Input
</h1> id="email"
{isResettingPassword ? ( type="email"
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} /> name="email"
) : ( register={register}
<form validations={{
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" required: "Email address is required",
onSubmit={handleSubmit(onSubmit)} validate: (value) =>
> /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
<div className="space-y-1"> value
<Input ) || "Email address is not valid",
id="email" }}
type="email" error={errors.email}
name="email" placeholder="Enter your email address..."
register={register} className="border-custom-border-300 h-[46px]"
validations={{ />
required: "Email address is required", </div>
validate: (value) => <div className="space-y-1">
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( <Input
value id="password"
) || "Email address is not valid", type="password"
}} name="password"
error={errors.email} register={register}
placeholder="Enter your email address..." validations={{
className="border-custom-border-300 h-[46px]" required: "Password is required",
/> }}
</div> error={errors.password}
<div className="space-y-1"> placeholder="Enter your password..."
<Input className="border-custom-border-300 h-[46px]"
id="password" />
type="password" </div>
name="password" <div className="text-right text-xs">
register={register} <button
validations={{ type="button"
required: "Password is required", onClick={() => setIsResettingPassword(true)}
}} className="text-custom-text-200 hover:text-custom-primary-100"
error={errors.password} >
placeholder="Enter your password..." Forgot your password?
className="border-custom-border-300 h-[46px]" </button>
/> </div>
</div> <div>
<div className="text-right text-xs"> <PrimaryButton
{isSignUpPage ? ( type="submit"
<Link href="/"> className="w-full text-center h-[46px]"
<a className="text-custom-text-200 hover:text-custom-primary-100"> disabled={!isValid && isDirty}
Already have an account? Sign in. loading={isSubmitting}
</a> >
</Link> {isSubmitting ? "Signing in..." : "Sign in"}
) : ( </PrimaryButton>
<button </div>
type="button" </form>
onClick={() => setIsResettingPassword(true)}
className="text-custom-text-200 hover:text-custom-primary-100"
>
Forgot your password?
</button>
)}
</div>
<div>
<PrimaryButton
type="submit"
className="w-full text-center h-[46px]"
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSignUpPage
? isSubmitting
? "Signing up..."
: "Sign up"
: isSubmitting
? "Signing in..."
: "Sign in"}
</PrimaryButton>
{!isSignUpPage && (
<Link href="/sign-up">
<a className="block text-custom-text-200 hover:text-custom-primary-100 text-xs mt-4">
Don{"'"}t have an account? Sign up.
</a>
</Link>
)}
</div>
</form>
)}
</> </>
); );
}; };

View File

@ -0,0 +1,114 @@
import React from "react";
import Link from "next/link";
import { useForm } from "react-hook-form";
// ui
import { Input, PrimaryButton } from "components/ui";
// types
type EmailPasswordFormValues = {
email: string;
password?: string;
confirm_password: string;
medium?: string;
};
type Props = {
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
};
export const EmailSignUpForm: React.FC<Props> = (props) => {
const { onSubmit } = props;
const {
register,
handleSubmit,
watch,
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailPasswordFormValues>({
defaultValues: {
email: "",
password: "",
confirm_password: "",
medium: "email",
},
mode: "onChange",
reValidateMode: "onChange",
});
return (
<>
<form
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
onSubmit={handleSubmit(onSubmit)}
>
<div className="space-y-1">
<Input
id="email"
type="email"
name="email"
register={register}
validations={{
required: "Email address is required",
validate: (value) =>
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
value
) || "Email address is not valid",
}}
error={errors.email}
placeholder="Enter your email address..."
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="space-y-1">
<Input
id="password"
type="password"
name="password"
register={register}
validations={{
required: "Password is required",
}}
error={errors.password}
placeholder="Enter your password..."
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="space-y-1">
<Input
id="confirm_password"
type="password"
name="confirm_password"
register={register}
validations={{
required: "Password is required",
validate: (val: string) => {
if (watch("password") != val) {
return "Your passwords do no match";
}
},
}}
error={errors.confirm_password}
placeholder="Confirm your password..."
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="text-right text-xs">
<Link href="/">
<a className="text-custom-text-200 hover:text-custom-primary-100">
Already have an account? Sign in.
</a>
</Link>
</div>
<div>
<PrimaryButton
type="submit"
className="w-full text-center h-[46px]"
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSubmitting ? "Signing up..." : "Sign up"}
</PrimaryButton>
</div>
</form>
</>
);
};

View File

@ -1,29 +1,27 @@
import { useEffect, useState, FC } from "react"; import { useEffect, useState, FC } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// next-themes
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// images // images
import githubBlackImage from "/public/logos/github-black.png"; import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png"; import githubWhiteImage from "/public/logos/github-white.png";
const { NEXT_PUBLIC_GITHUB_ID } = process.env;
export interface GithubLoginButtonProps { export interface GithubLoginButtonProps {
handleSignIn: React.Dispatch<string>; handleSignIn: React.Dispatch<string>;
clientId: string;
} }
export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => { export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
const { handleSignIn, clientId } = props;
// states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
const [gitCode, setGitCode] = useState<null | string>(null); const [gitCode, setGitCode] = useState<null | string>(null);
// router
const { const {
query: { code }, query: { code },
} = useRouter(); } = useRouter();
// theme
const { theme } = useTheme(); const { theme } = useTheme();
useEffect(() => { useEffect(() => {
@ -42,7 +40,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn })
return ( return (
<div className="w-full flex justify-center items-center"> <div className="w-full flex justify-center items-center">
<Link <Link
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`} href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
> >
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]"> <button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
<Image <Image

View File

@ -1,22 +1,23 @@
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react"; import { FC, useEffect, useRef, useCallback, useState } from "react";
import Script from "next/script"; import Script from "next/script";
export interface IGoogleLoginButton { export interface IGoogleLoginButton {
text?: string;
handleSignIn: React.Dispatch<any>; handleSignIn: React.Dispatch<any>;
styles?: CSSProperties; clientId: string;
} }
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => { export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
const { handleSignIn, clientId } = props;
// refs
const googleSignInButton = useRef<HTMLDivElement>(null); const googleSignInButton = useRef<HTMLDivElement>(null);
// states
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false); const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
const loadScript = useCallback(() => { const loadScript = useCallback(() => {
if (!googleSignInButton.current || gsiScriptLoaded) return; if (!googleSignInButton.current || gsiScriptLoaded) return;
window?.google?.accounts.id.initialize({ window?.google?.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "", client_id: clientId,
callback: handleSignIn, callback: handleSignIn,
}); });
@ -39,7 +40,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
window?.google?.accounts.id.prompt(); // also display the One Tap dialog window?.google?.accounts.id.prompt(); // also display the One Tap dialog
setGsiScriptLoaded(true); setGsiScriptLoaded(true);
}, [handleSignIn, gsiScriptLoaded]); }, [handleSignIn, gsiScriptLoaded, clientId]);
useEffect(() => { useEffect(() => {
if (window?.google?.accounts?.id) { if (window?.google?.accounts?.id) {

View File

@ -3,3 +3,4 @@ export * from "./email-password-form";
export * from "./email-reset-password-form"; export * from "./email-reset-password-form";
export * from "./github-login-button"; export * from "./github-login-button";
export * from "./google-login"; export * from "./google-login";
export * from "./email-signup-form";

View File

@ -13,9 +13,14 @@ import { IProject } from "types";
type Props = { type Props = {
projectDetails: IProject | undefined; projectDetails: IProject | undefined;
handleChange: (formData: Partial<IProject>) => Promise<void>; handleChange: (formData: Partial<IProject>) => Promise<void>;
disabled?: boolean;
}; };
export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => { export const AutoArchiveAutomation: React.FC<Props> = ({
projectDetails,
handleChange,
disabled = false,
}) => {
const [monthModal, setmonthModal] = useState(false); const [monthModal, setmonthModal] = useState(false);
const initialValues: Partial<IProject> = { archive_in: 1 }; const initialValues: Partial<IProject> = { archive_in: 1 };
@ -49,6 +54,7 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
: handleChange({ archive_in: 0 }) : handleChange({ archive_in: 0 })
} }
size="sm" size="sm"
disabled={disabled}
/> />
</div> </div>
@ -70,6 +76,7 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
input input
verticalPosition="bottom" verticalPosition="bottom"
width="w-full" width="w-full"
disabled={disabled}
> >
<> <>
{PROJECT_AUTOMATION_MONTHS.map((month) => ( {PROJECT_AUTOMATION_MONTHS.map((month) => (

View File

@ -24,9 +24,14 @@ import { getStatesList } from "helpers/state.helper";
type Props = { type Props = {
projectDetails: IProject | undefined; projectDetails: IProject | undefined;
handleChange: (formData: Partial<IProject>) => Promise<void>; handleChange: (formData: Partial<IProject>) => Promise<void>;
disabled?: boolean;
}; };
export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => { export const AutoCloseAutomation: React.FC<Props> = ({
projectDetails,
handleChange,
disabled = false,
}) => {
const [monthModal, setmonthModal] = useState(false); const [monthModal, setmonthModal] = useState(false);
const router = useRouter(); const router = useRouter();
@ -98,6 +103,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
: handleChange({ close_in: 0, default_state: null }) : handleChange({ close_in: 0, default_state: null })
} }
size="sm" size="sm"
disabled={disabled}
/> />
</div> </div>
@ -119,6 +125,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
}} }}
input input
width="w-full" width="w-full"
disabled={disabled}
> >
<> <>
{PROJECT_AUTOMATION_MONTHS.map((month) => ( {PROJECT_AUTOMATION_MONTHS.map((month) => (

View File

@ -1,32 +1,23 @@
import React, { useEffect, useState, useRef, useCallback } from "react"; import React, { useEffect, useState, useRef, useCallback } from "react";
// next
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// swr
import useSWR from "swr"; import useSWR from "swr";
// react-dropdown
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
// headless ui
import { Tab, Transition, Popover } from "@headlessui/react"; import { Tab, Transition, Popover } from "@headlessui/react";
// services // services
import fileService from "services/file.service"; import fileService from "services/file.service";
// components
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
// hooks // hooks
import useWorkspaceDetails from "hooks/use-workspace-details"; import useWorkspaceDetails from "hooks/use-workspace-details";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
const unsplashEnabled = import { Input, PrimaryButton, SecondaryButton, Loader } from "components/ui";
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "1";
const tabOptions = [ const tabOptions = [
{
key: "unsplash",
title: "Unsplash",
},
{ {
key: "images", key: "images",
title: "Images", title: "Images",
@ -64,8 +55,22 @@ export const ImagePickerPopover: React.FC<Props> = ({
search: "", search: "",
}); });
const { data: images } = useSWR(`UNSPLASH_IMAGES_${searchParams}`, () => const { data: unsplashImages, error: unsplashError } = useSWR(
fileService.getUnsplashImages(1, searchParams) `UNSPLASH_IMAGES_${searchParams}`,
() => fileService.getUnsplashImages(searchParams),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
const { data: projectCoverImages } = useSWR(
`PROJECT_COVER_IMAGES`,
() => fileService.getProjectCoverImages(),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
); );
const imagePickerRef = useRef<HTMLDivElement>(null); const imagePickerRef = useRef<HTMLDivElement>(null);
@ -115,18 +120,17 @@ export const ImagePickerPopover: React.FC<Props> = ({
}; };
useEffect(() => { useEffect(() => {
if (!images || value !== null) return; if (!unsplashImages || value !== null) return;
onChange(images[0].urls.regular);
}, [value, onChange, images]); onChange(unsplashImages[0].urls.regular);
}, [value, onChange, unsplashImages]);
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false)); useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
if (!unsplashEnabled) return null;
return ( return (
<Popover className="relative z-[2]" ref={ref}> <Popover className="relative z-[2]" ref={ref}>
<Popover.Button <Popover.Button
className="rounded-sm border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100" className="rounded border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
disabled={disabled} disabled={disabled}
> >
@ -141,15 +145,19 @@ export const ImagePickerPopover: React.FC<Props> = ({
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg"> <Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-sm">
<div <div
ref={imagePickerRef} ref={imagePickerRef}
className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl" className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"
> >
<Tab.Group> <Tab.Group>
<div> <Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1"> {tabOptions.map((tab) => {
{tabOptions.map((tab) => ( if (!unsplashImages && unsplashError && tab.key === "unsplash") return null;
if (projectCoverImages && projectCoverImages.length === 0 && tab.key === "images")
return null;
return (
<Tab <Tab
key={tab.key} key={tab.key}
className={({ selected }) => className={({ selected }) =>
@ -160,50 +168,106 @@ export const ImagePickerPopover: React.FC<Props> = ({
> >
{tab.title} {tab.title}
</Tab> </Tab>
))} );
</Tab.List> })}
</div> </Tab.List>
<Tab.Panels className="h-full w-full flex-1 overflow-y-auto overflow-x-hidden"> <Tab.Panels className="h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
<Tab.Panel className="h-full w-full space-y-4"> {(unsplashImages || !unsplashError) && (
<div className="flex gap-x-2 pt-7"> <Tab.Panel className="h-full w-full space-y-4 mt-4">
<Input <div className="flex gap-x-2">
name="search" <Input
className="text-sm" name="search"
id="search" className="text-sm"
value={formData.search} id="search"
onChange={(e) => setFormData({ ...formData, search: e.target.value })} value={formData.search}
placeholder="Search for images" onChange={(e) => setFormData({ ...formData, search: e.target.value })}
/> placeholder="Search for images"
<PrimaryButton onClick={() => setSearchParams(formData.search)} size="sm"> />
Search <PrimaryButton onClick={() => setSearchParams(formData.search)} size="sm">
</PrimaryButton> Search
</div> </PrimaryButton>
{images ? ( </div>
<div className="grid grid-cols-4 gap-4"> {unsplashImages ? (
{images.map((image) => ( unsplashImages.length > 0 ? (
<div <div className="grid grid-cols-4 gap-4">
key={image.id} {unsplashImages.map((image) => (
className="relative col-span-2 aspect-video md:col-span-1" <div
> key={image.id}
<img className="relative col-span-2 aspect-video md:col-span-1"
src={image.urls.small} onClick={() => {
alt={image.alt_description} setIsOpen(false);
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover" onChange(image.urls.regular);
onClick={() => { }}
setIsOpen(false); >
onChange(image.urls.regular); <img
}} src={image.urls.small}
/> alt={image.alt_description}
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
/>
</div>
))}
</div> </div>
))} ) : (
</div> <p className="text-center text-custom-text-300 text-xs pt-7">
) : ( No images found.
<div className="flex justify-center pt-20"> </p>
<Spinner /> )
</div> ) : (
)} <Loader className="grid grid-cols-4 gap-4">
</Tab.Panel> <Loader.Item height="80px" width="100%" />
<Tab.Panel className="h-full w-full pt-5"> <Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
</Loader>
)}
</Tab.Panel>
)}
{(!projectCoverImages || projectCoverImages.length !== 0) && (
<Tab.Panel className="h-full w-full space-y-4 mt-4">
{projectCoverImages ? (
projectCoverImages.length > 0 ? (
<div className="grid grid-cols-4 gap-4">
{projectCoverImages.map((image, index) => (
<div
key={image}
className="relative col-span-2 aspect-video md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image);
}}
>
<img
src={image}
alt={`Default project cover image- ${index}`}
className="cursor-pointer rounded absolute top-0 left-0 h-full w-full object-cover"
/>
</div>
))}
</div>
) : (
<p className="text-center text-custom-text-300 text-xs pt-7">
No images found.
</p>
)
) : (
<Loader className="grid grid-cols-4 gap-4 pt-4">
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
</Loader>
)}
</Tab.Panel>
)}
<Tab.Panel className="h-full w-full mt-4">
<div className="w-full h-full flex flex-col gap-y-2"> <div className="w-full h-full flex flex-col gap-y-2">
<div className="flex items-center gap-3 w-full flex-1"> <div className="flex items-center gap-3 w-full flex-1">
<div <div

View File

@ -15,10 +15,28 @@ export const StateGroupBacklogIcon: React.FC<Props> = ({
height={height} height={height}
width={width} width={width}
className={className} className={className}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 323.15 323.03"
> >
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" strokeDasharray="4 4" /> <g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
fill={color}
d="M163.42,322.92A172.12,172.12,0,0,1,104.8,312.7c-3.92-1.4-5.22-3.05-3.07-7.1,2.4-4.52,3-11.38,6.64-13.48s9.34,2.47,14.23,3.81c29.55,8.11,58.78,7.25,87.57-3.31,4.08-1.5,5.86-1.05,7.09,3.21a82.63,82.63,0,0,0,4.6,11c1.19,2.57,1,4.06-2,5.2a163.84,163.84,0,0,1-40.05,9.76C173.84,322.45,167.89,323.34,163.42,322.92Z"
/>
<path
fill={color}
d="M.07,163a174.76,174.76,0,0,1,10.07-58c1.59-4.57,3.53-5.59,7.8-3.2a61,61,0,0,0,10.11,4.19c3.11,1.06,4.07,2.46,2.71,5.79-6.43,15.73-9.17,32.33-9.23,49.14a132.65,132.65,0,0,0,8.17,47.35c2.44,6.5,2.33,6.57-4.06,9.35-3.35,1.45-6.83,2.63-10.11,4.23-2.44,1.19-3.54.49-4.43-1.86a162.3,162.3,0,0,1-10-41C.51,173.12-.24,167.17.07,163Z"
/>
<path
fill={color}
d="M323,160.16a169.68,169.68,0,0,1-10.2,58.09c-1.45,4.08-3.21,5.07-7.14,3a105.3,105.3,0,0,0-11.48-4.81c-2.23-.85-3.2-1.85-2.16-4.41a133.86,133.86,0,0,0,9.57-48.59,132,132,0,0,0-8.9-50.69c-1.67-4.24-.8-5.79,3.29-7a84,84,0,0,0,11-4.62c2.65-1.24,4.05-.82,5.16,2.12a159.68,159.68,0,0,1,9.68,39C322.56,148.71,323.52,155.17,323,160.16Z"
/>
<path
fill={color}
d="M161.59,0a164.28,164.28,0,0,1,58,10.72c2.81,1,3.75,2,2.41,4.93-2,4.38-3.86,8.84-5.5,13.37-.93,2.56-2.28,2.77-4.53,1.87a137.94,137.94,0,0,0-99.35-.52c-3.43,1.32-5.3,1.35-6.45-2.69a50.33,50.33,0,0,0-4.55-11c-2.25-3.93-.36-5.11,2.9-6.29A165.32,165.32,0,0,1,161.59,0Z"
/>
</g>
</g>
</svg> </svg>
); );

View File

@ -9,17 +9,38 @@ export const StateGroupStartedIcon: React.FC<Props> = ({
width = "20", width = "20",
height = "20", height = "20",
className, className,
color = "#f59e0b", color = "#f39e1f",
}) => ( }) => (
<svg <svg
height={height} height={height}
width={width} width={width}
className={className} className={className}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 12 12" viewBox="0 0 152.93 152.95"
fill="none"
> >
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" /> <g id="Layer_2" data-name="Layer 2">
<circle cx="6" cy="6" r="3.35" stroke={color} strokeWidth="0.8" strokeDasharray="2.4 2.4" /> <g id="Layer_1-2" data-name="Layer 1">
<path
fill={color}
d="M77.74,0C35.63-.62.78,32.9,0,74.94c-.77,42.74,33,77.34,76.23,78A76.48,76.48,0,0,0,77.74,0ZM75.46,142.68a66.24,66.24,0,1,1,3-132.45c35.71,1,66.31,31.26,64.16,70.08A66.23,66.23,0,0,1,75.46,142.68Z"
/>
<path
fill={color}
d="M124.29,76.58a49.52,49.52,0,0,1-3.11,16.9c-.38,1-.77,1.27-1.81.78-2.15-1-4.34-1.92-6.56-2.72-1.3-.46-1.51-1-1-2.3a36.61,36.61,0,0,0,.64-23.77c-1-3.48-1.06-3.47,2.38-4.88,1.57-.65,3.15-1.27,4.68-2,.94-.44,1.34-.22,1.69.75A49.74,49.74,0,0,1,124.29,76.58Z"
/>
<path
fill={color}
d="M94.65,32.63c-.1.22-.19.42-.27.63-1,2.5-2.08,5-3.09,7.51-.28.69-.55.89-1.37.59a37.3,37.3,0,0,0-26.82,0c-.91.34-1.15.08-1.46-.7-1-2.46-2-4.92-3.06-7.34-.42-.92-.07-1.18.69-1.46a47.66,47.66,0,0,1,34.43,0C94.06,32,94.68,32,94.65,32.63Z"
/>
<path
fill={color}
d="M28.72,76.67a48.27,48.27,0,0,1,3-17.13c.45-1.25.92-1.34,2-.83,2.25,1,4.56,2,6.87,2.87.86.34,1.05.67.71,1.58a36.85,36.85,0,0,0-.07,26.36c.36,1,.3,1.46-.75,1.86-2.38.9-4.72,1.88-7,2.92-1,.43-1.33.2-1.68-.76A46.76,46.76,0,0,1,28.72,76.67Z"
/>
<path
fill={color}
d="M76.37,124.22a48.11,48.11,0,0,1-16.91-3.08c-1.05-.38-1.26-.8-.79-1.82,1-2.31,2-4.66,2.93-7,.34-.87.69-1.06,1.61-.72a37.06,37.06,0,0,0,26.67,0c.75-.28,1.09-.23,1.39.55,1,2.56,2,5.13,3.18,7.65.49,1.08-.3,1.13-.86,1.34A46.53,46.53,0,0,1,76.37,124.22Z"
/>
</g>
</g>
</svg> </svg>
); );

View File

@ -1,7 +1,8 @@
import React from "react"; import React from "react";
// next imports // next imports
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// swr
import { mutate } from "swr";
// lucide icons // lucide icons
import { import {
ChevronDown, ChevronDown,
@ -13,6 +14,7 @@ import {
Loader, Loader,
} from "lucide-react"; } from "lucide-react";
// components // components
import { IssuePeekOverview } from "components/issues/peek-overview";
import { SubIssuesRootList } from "./issues-list"; import { SubIssuesRootList } from "./issues-list";
import { IssueProperty } from "./properties"; import { IssueProperty } from "./properties";
// ui // ui
@ -20,6 +22,8 @@ import { Tooltip, CustomMenu } from "components/ui";
// types // types
import { ICurrentUserResponse, IIssue } from "types"; import { ICurrentUserResponse, IIssue } from "types";
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
// fetch keys
import { SUB_ISSUES } from "constants/fetch-keys";
export interface ISubIssues { export interface ISubIssues {
workspaceSlug: string; workspaceSlug: string;
@ -38,7 +42,6 @@ export interface ISubIssues {
issueId: string, issueId: string,
issue?: IIssue | null issue?: IIssue | null
) => void; ) => void;
setPeekParentId: (id: string) => void;
} }
export const SubIssues: React.FC<ISubIssues> = ({ export const SubIssues: React.FC<ISubIssues> = ({
@ -54,14 +57,12 @@ export const SubIssues: React.FC<ISubIssues> = ({
handleIssuesLoader, handleIssuesLoader,
copyText, copyText,
handleIssueCrudOperation, handleIssueCrudOperation,
setPeekParentId,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { query } = router;
const { peekIssue } = query as { peekIssue: string };
const openPeekOverview = (issue_id: string) => { const openPeekOverview = (issue_id: string) => {
const { query } = router;
setPeekParentId(parentIssue?.id);
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query, peekIssue: issue_id }, query: { ...query, peekIssue: issue_id },
@ -199,7 +200,17 @@ export const SubIssues: React.FC<ISubIssues> = ({
handleIssuesLoader={handleIssuesLoader} handleIssuesLoader={handleIssuesLoader}
copyText={copyText} copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation} handleIssueCrudOperation={handleIssueCrudOperation}
setPeekParentId={setPeekParentId} />
)}
{peekIssue && peekIssue === issue?.id && (
<IssuePeekOverview
handleMutation={() =>
parentIssue && parentIssue?.id && mutate(SUB_ISSUES(parentIssue?.id))
}
projectId={issue?.project ?? ""}
workspaceSlug={workspaceSlug ?? ""}
readOnly={!editable}
/> />
)} )}
</div> </div>

View File

@ -27,7 +27,6 @@ export interface ISubIssuesRootList {
issueId: string, issueId: string,
issue?: IIssue | null issue?: IIssue | null
) => void; ) => void;
setPeekParentId: (id: string) => void;
} }
export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
@ -42,7 +41,6 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
handleIssuesLoader, handleIssuesLoader,
copyText, copyText,
handleIssueCrudOperation, handleIssueCrudOperation,
setPeekParentId,
}) => { }) => {
const { data: issues, isLoading } = useSWR( const { data: issues, isLoading } = useSWR(
workspaceSlug && projectId && parentIssue && parentIssue?.id workspaceSlug && projectId && parentIssue && parentIssue?.id
@ -83,7 +81,6 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
handleIssuesLoader={handleIssuesLoader} handleIssuesLoader={handleIssuesLoader}
copyText={copyText} copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation} handleIssueCrudOperation={handleIssueCrudOperation}
setPeekParentId={setPeekParentId}
/> />
))} ))}

View File

@ -10,7 +10,6 @@ import { ExistingIssuesListModal } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { SubIssuesRootList } from "./issues-list"; import { SubIssuesRootList } from "./issues-list";
import { ProgressBar } from "./progressbar"; import { ProgressBar } from "./progressbar";
import { IssuePeekOverview } from "components/issues/peek-overview";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// hooks // hooks
@ -60,8 +59,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
: null : null
); );
const [peekParentId, setPeekParentId] = React.useState<string | null>("");
const [issuesLoader, setIssuesLoader] = React.useState<ISubIssuesRootLoaders>({ const [issuesLoader, setIssuesLoader] = React.useState<ISubIssuesRootLoaders>({
visibility: [parentIssue?.id], visibility: [parentIssue?.id],
delete: [], delete: [],
@ -237,7 +234,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
handleIssuesLoader={handleIssuesLoader} handleIssuesLoader={handleIssuesLoader}
copyText={copyText} copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation} handleIssueCrudOperation={handleIssueCrudOperation}
setPeekParentId={setPeekParentId}
/> />
</div> </div>
)} )}
@ -363,13 +359,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
)} )}
</> </>
)} )}
<IssuePeekOverview
handleMutation={() => peekParentId && peekIssue && mutateSubIssues(peekParentId)}
projectId={projectId ?? ""}
workspaceSlug={workspaceSlug ?? ""}
readOnly={!isEditable}
/>
</div> </div>
); );
}; };

View File

@ -393,7 +393,7 @@ export const CreateProjectModal: React.FC<Props> = ({
value={value} value={value}
onChange={onChange} onChange={onChange}
options={options} options={options}
buttonClassName="!px-2 shadow-md" buttonClassName="border-[0.5px] !px-2 shadow-md"
label={ label={
<div className="flex items-center justify-center gap-2 py-[1px]"> <div className="flex items-center justify-center gap-2 py-[1px]">
{value ? ( {value ? (

View File

@ -16,9 +16,10 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
value: any; value: any;
onChange: (val: string) => void; onChange: (val: string) => void;
isDisabled?: boolean;
}; };
export const MemberSelect: React.FC<Props> = ({ value, onChange }) => { export const MemberSelect: React.FC<Props> = ({ value, onChange, isDisabled = false }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -79,6 +80,7 @@ export const MemberSelect: React.FC<Props> = ({ value, onChange }) => {
position="right" position="right"
width="w-full" width="w-full"
onChange={onChange} onChange={onChange}
disabled={isDisabled}
/> />
); );
}; };

View File

@ -89,7 +89,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
onClick={() => { onClick={() => {
editor?.chain().focus().run(); editor?.chain().focus().run();
}} }}
className={`tiptap-editor-container cursor-text ${editorClassNames}`} className={`tiptap-editor-container relative cursor-text ${editorClassNames}`}
> >
{editor && <EditorBubbleMenu editor={editor} />} {editor && <EditorBubbleMenu editor={editor} />}
<div className={`${editorContentCustomClassNames}`}> <div className={`${editorContentCustomClassNames}`}>

View File

@ -80,8 +80,6 @@ export const TableMenu = ({ editor }: { editor: any }) => {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const tableNode = findTableAncestor(range.startContainer); const tableNode = findTableAncestor(range.startContainer);
let parent = tableNode?.parentElement;
if (tableNode) { if (tableNode) {
const tableRect = tableNode.getBoundingClientRect(); const tableRect = tableNode.getBoundingClientRect();
const tableCenter = tableRect.left + tableRect.width / 2; const tableCenter = tableRect.left + tableRect.width / 2;
@ -90,18 +88,6 @@ export const TableMenu = ({ editor }: { editor: any }) => {
const tableBottom = tableRect.bottom; const tableBottom = tableRect.bottom;
setTableLocation({ bottom: tableBottom, left: menuLeft }); setTableLocation({ bottom: tableBottom, left: menuLeft });
while (parent) {
if (!parent.classList.contains("disable-scroll"))
parent.classList.add("disable-scroll");
parent = parent.parentElement;
}
} else {
const scrollDisabledContainers = document.querySelectorAll(".disable-scroll");
scrollDisabledContainers.forEach((container) => {
container.classList.remove("disable-scroll");
});
} }
} }
}; };
@ -115,13 +101,9 @@ export const TableMenu = ({ editor }: { editor: any }) => {
return ( return (
<section <section
className={`fixed left-1/2 transform -translate-x-1/2 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 shadow-custom-shadow-sm p-1 ${ className={`absolute z-20 left-1/2 -translate-x-1/2 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 shadow-custom-shadow-sm p-1 ${
isOpen ? "block" : "hidden" isOpen ? "block" : "hidden"
}`} }`}
style={{
bottom: `calc(100vh - ${tableLocation.bottom + 45}px)`,
left: `${tableLocation.left}px`,
}}
> >
{items.map((item, index) => ( {items.map((item, index) => (
<Tooltip key={index} tooltipContent={item.name}> <Tooltip key={index} tooltipContent={item.name}>

View File

@ -46,6 +46,7 @@ const CustomMenu = ({
type="button" type="button"
onClick={menuButtonOnClick} onClick={menuButtonOnClick}
className={customButtonClassName} className={customButtonClassName}
disabled={disabled}
> >
{customButton} {customButton}
</Menu.Button> </Menu.Button>

View File

@ -16,6 +16,7 @@ type Props = {
}; };
secondaryButton?: React.ReactNode; secondaryButton?: React.ReactNode;
isFullScreen?: boolean; isFullScreen?: boolean;
disabled?: boolean;
}; };
export const EmptyState: React.FC<Props> = ({ export const EmptyState: React.FC<Props> = ({
@ -25,6 +26,7 @@ export const EmptyState: React.FC<Props> = ({
primaryButton, primaryButton,
secondaryButton, secondaryButton,
isFullScreen = true, isFullScreen = true,
disabled = false,
}) => ( }) => (
<div <div
className={`h-full w-full mx-auto grid place-items-center p-8 ${ className={`h-full w-full mx-auto grid place-items-center p-8 ${
@ -37,7 +39,11 @@ export const EmptyState: React.FC<Props> = ({
{description && <p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>} {description && <p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{primaryButton && ( {primaryButton && (
<PrimaryButton className="flex items-center gap-1.5" onClick={primaryButton.onClick}> <PrimaryButton
className="flex items-center gap-1.5"
onClick={primaryButton.onClick}
disabled={disabled}
>
{primaryButton.icon} {primaryButton.icon}
{primaryButton.text} {primaryButton.text}
</PrimaryButton> </PrimaryButton>

View File

@ -21,7 +21,7 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
size === "sm" ? "h-4 w-6" : size === "md" ? "h-5 w-8" : "h-6 w-10" size === "sm" ? "h-4 w-6" : size === "md" ? "h-5 w-8" : "h-6 w-10"
} flex-shrink-0 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${ } flex-shrink-0 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
value ? "bg-custom-primary-100" : "bg-gray-700" value ? "bg-custom-primary-100" : "bg-gray-700"
} ${className || ""}`} } ${className || ""} ${disabled ? "cursor-not-allowed" : ""}`}
> >
<span className="sr-only">{label}</span> <span className="sr-only">{label}</span>
<span <span
@ -36,7 +36,7 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
? "translate-x-4" ? "translate-x-4"
: "translate-x-5") + " bg-white" : "translate-x-5") + " bg-white"
: "translate-x-0.5 bg-custom-background-90" : "translate-x-0.5 bg-custom-background-90"
}`} } ${disabled ? "cursor-not-allowed" : ""}`}
/> />
</Switch> </Switch>
); );

View File

@ -1,24 +1,23 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
// headless ui
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import useTheme from "hooks/use-theme";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// icons // icons
import { Bolt, HelpOutlineOutlined, WestOutlined } from "@mui/icons-material"; import { Bolt, HelpOutlineOutlined, WestOutlined } from "@mui/icons-material";
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline"; import { DiscordIcon } from "components/icons";
import { DocumentIcon, DiscordIcon, GithubIcon } from "components/icons"; import { FileText, Github, MessagesSquare } from "lucide-react";
// mobx store // assets
import { useMobxStore } from "lib/mobx/store-provider"; import packageJson from "package.json";
const helpOptions = [ const helpOptions = [
{ {
name: "Documentation", name: "Documentation",
href: "https://docs.plane.so/", href: "https://docs.plane.so/",
Icon: DocumentIcon, Icon: FileText,
}, },
{ {
name: "Join our Discord", name: "Join our Discord",
@ -28,13 +27,13 @@ const helpOptions = [
{ {
name: "Report a bug", name: "Report a bug",
href: "https://github.com/makeplane/plane/issues/new/choose", href: "https://github.com/makeplane/plane/issues/new/choose",
Icon: GithubIcon, Icon: Github,
}, },
{ {
name: "Chat with us", name: "Chat with us",
href: null, href: null,
onClick: () => (window as any).$crisp.push(["do", "chat:show"]), onClick: () => (window as any).$crisp.push(["do", "chat:show"]),
Icon: ChatBubbleOvalLeftEllipsisIcon, Icon: MessagesSquare,
}, },
]; ];
@ -123,37 +122,44 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<div <div
className={`absolute bottom-2 ${ className={`absolute bottom-2 min-w-[10rem] ${
store?.theme?.sidebarCollapsed ? "left-full" : "left-[-75px]" store?.theme?.sidebarCollapsed ? "left-full" : "-left-[75px]"
} space-y-2 rounded-sm bg-custom-background-80 p-1 shadow-md`} } rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs whitespace-nowrap divide-y divide-custom-border-200`}
ref={helpOptionsRef} ref={helpOptionsRef}
> >
{helpOptions.map(({ name, Icon, href, onClick }) => { <div className="space-y-1 pb-2">
if (href) {helpOptions.map(({ name, Icon, href, onClick }) => {
return ( if (href)
<Link href={href} key={name}> return (
<a <Link href={href} key={name}>
target="_blank" <a
className="flex items-center gap-x-2 whitespace-nowrap rounded-md px-2 py-1 text-xs hover:bg-custom-background-90" target="_blank"
className="flex items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80"
>
<div className="grid place-items-center flex-shrink-0">
<Icon className="text-custom-text-200 h-3.5 w-3.5" size={14} />
</div>
<span className="text-xs">{name}</span>
</a>
</Link>
);
else
return (
<button
key={name}
type="button"
onClick={onClick ?? undefined}
className="flex w-full items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80"
> >
<Icon className="h-4 w-4 text-custom-text-200" /> <div className="grid place-items-center flex-shrink-0">
<span className="text-sm">{name}</span> <Icon className="text-custom-text-200 h-3.5 w-3.5" size={14} />
</a> </div>
</Link> <span className="text-xs">{name}</span>
); </button>
else );
return ( })}
<button </div>
key={name} <div className="px-2 pt-2 pb-1 text-[10px]">Version: v{packageJson.version}</div>
type="button"
onClick={onClick ? onClick : undefined}
className="flex w-full items-center gap-x-2 whitespace-nowrap rounded-md px-2 py-1 text-xs hover:bg-custom-background-90"
>
<Icon className="h-4 w-4 text-custom-sidebar-text-200" />
<span className="text-sm">{name}</span>
</button>
);
})}
</div> </div>
</Transition> </Transition>
</div> </div>

View File

@ -15,6 +15,7 @@ const nextConfig = {
"vinci-web.s3.amazonaws.com", "vinci-web.s3.amazonaws.com",
"planefs-staging.s3.ap-south-1.amazonaws.com", "planefs-staging.s3.ap-south-1.amazonaws.com",
"planefs.s3.amazonaws.com", "planefs.s3.amazonaws.com",
"planefs-staging.s3.amazonaws.com",
"images.unsplash.com", "images.unsplash.com",
"avatars.githubusercontent.com", "avatars.githubusercontent.com",
"localhost", "localhost",

View File

@ -1,6 +1,6 @@
{ {
"name": "web", "name": "web",
"version": "0.1.0", "version": "0.13.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --port 3000", "dev": "next dev --port 3000",

View File

@ -2,7 +2,7 @@ import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import useSWR, { mutate } from "swr";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
@ -21,7 +21,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { IProject } from "types"; import { IProject } from "types";
// constant // constant
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys";
// helper // helper
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
@ -34,6 +34,13 @@ const AutomationsSettings: NextPage = () => {
const { projectDetails } = useProjectDetails(); const { projectDetails } = useProjectDetails();
const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
: null
);
const handleChange = async (formData: Partial<IProject>) => { const handleChange = async (formData: Partial<IProject>) => {
if (!workspaceSlug || !projectId || !projectDetails) return; if (!workspaceSlug || !projectId || !projectDetails) return;
@ -62,6 +69,8 @@ const AutomationsSettings: NextPage = () => {
}); });
}; };
const isAdmin = memberDetails?.role === 20;
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
@ -79,12 +88,20 @@ const AutomationsSettings: NextPage = () => {
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0"> <div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
<SettingsSidebar /> <SettingsSidebar />
</div> </div>
<section className="pr-9 py-8 w-full overflow-y-auto"> <section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Automations</h3> <h3 className="text-xl font-medium">Automations</h3>
</div> </div>
<AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} /> <AutoArchiveAutomation
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} /> projectDetails={projectDetails}
handleChange={handleChange}
disabled={!isAdmin}
/>
<AutoCloseAutomation
projectDetails={projectDetails}
handleChange={handleChange}
disabled={!isAdmin}
/>
</section> </section>
</div> </div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>

View File

@ -25,7 +25,7 @@ import { ContrastOutlined } from "@mui/icons-material";
import { IProject } from "types"; import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys";
// helper // helper
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
@ -102,6 +102,13 @@ const FeaturesSettings: NextPage = () => {
: null : null
); );
const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
: null
);
const handleSubmit = async (formData: Partial<IProject>) => { const handleSubmit = async (formData: Partial<IProject>) => {
if (!workspaceSlug || !projectId || !projectDetails) return; if (!workspaceSlug || !projectId || !projectDetails) return;
@ -140,6 +147,8 @@ const FeaturesSettings: NextPage = () => {
); );
}; };
const isAdmin = memberDetails?.role === 20;
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
@ -157,7 +166,7 @@ const FeaturesSettings: NextPage = () => {
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0"> <div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
<SettingsSidebar /> <SettingsSidebar />
</div> </div>
<section className="pr-9 py-8 w-full overflow-y-auto"> <section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Features</h3> <h3 className="text-xl font-medium">Features</h3>
</div> </div>
@ -199,6 +208,7 @@ const FeaturesSettings: NextPage = () => {
[feature.property]: !projectDetails?.[feature.property as keyof IProject], [feature.property]: !projectDetails?.[feature.property as keyof IProject],
}); });
}} }}
disabled={!isAdmin}
size="sm" size="sm"
/> />
</div> </div>

View File

@ -22,7 +22,7 @@ import emptyIntegration from "public/empty-state/integration.svg";
import { IProject } from "types"; import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; import { PROJECT_DETAILS, USER_PROJECT_VIEW, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
// helper // helper
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
@ -45,6 +45,15 @@ const ProjectIntegrations: NextPage = () => {
: null : null
); );
const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
: null
);
const isAdmin = memberDetails?.role === 20;
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
@ -62,7 +71,7 @@ const ProjectIntegrations: NextPage = () => {
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0"> <div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
<SettingsSidebar /> <SettingsSidebar />
</div> </div>
<div className="pr-9 py-8 gap-10 w-full overflow-y-auto"> <div className={`pr-9 py-8 gap-10 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Integrations</h3> <h3 className="text-xl font-medium">Integrations</h3>
</div> </div>
@ -85,6 +94,7 @@ const ProjectIntegrations: NextPage = () => {
text: "Configure now", text: "Configure now",
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`), onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
}} }}
disabled={!isAdmin}
/> />
) )
) : ( ) : (

View File

@ -43,6 +43,7 @@ import {
PROJECT_INVITATIONS_WITH_EMAIL, PROJECT_INVITATIONS_WITH_EMAIL,
PROJECT_MEMBERS, PROJECT_MEMBERS,
PROJECT_MEMBERS_WITH_EMAIL, PROJECT_MEMBERS_WITH_EMAIL,
USER_PROJECT_VIEW,
WORKSPACE_DETAILS, WORKSPACE_DETAILS,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// constants // constants
@ -111,6 +112,13 @@ const MembersSettings: NextPage = () => {
: null : null
); );
const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
: null
);
const members = [ const members = [
...(projectMembers?.map((item) => ({ ...(projectMembers?.map((item) => ({
id: item.id, id: item.id,
@ -212,6 +220,8 @@ const MembersSettings: NextPage = () => {
}); });
}; };
const isAdmin = memberDetails?.role === 20;
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
@ -277,7 +287,7 @@ const MembersSettings: NextPage = () => {
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0"> <div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
<SettingsSidebar /> <SettingsSidebar />
</div> </div>
<section className="pr-9 py-8 w-full overflow-y-auto"> <section className={`pr-9 py-8 w-full overflow-y-auto`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Defaults</h3> <h3 className="text-xl font-medium">Defaults</h3>
</div> </div>
@ -296,6 +306,7 @@ const MembersSettings: NextPage = () => {
onChange={(val: string) => { onChange={(val: string) => {
submitChanges({ project_lead: val }); submitChanges({ project_lead: val });
}} }}
isDisabled={!isAdmin}
/> />
)} )}
/> />
@ -320,6 +331,7 @@ const MembersSettings: NextPage = () => {
onChange={(val: string) => { onChange={(val: string) => {
submitChanges({ default_assignee: val }); submitChanges({ default_assignee: val });
}} }}
isDisabled={!isAdmin}
/> />
)} )}
/> />
@ -467,7 +479,7 @@ const MembersSettings: NextPage = () => {
); );
})} })}
</CustomSelect> </CustomSelect>
<CustomMenu ellipsis> <CustomMenu ellipsis disabled={!isAdmin}>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => { onClick={() => {
if (member.member) setSelectedRemoveMember(member.id); if (member.member) setSelectedRemoveMember(member.id);

View File

@ -1,13 +1,14 @@
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { useTheme } from "next-themes";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// services // services
import authenticationService from "services/authentication.service"; import authenticationService from "services/authentication.service";
import { AppConfigService } from "services/app-config.service";
// hooks // hooks
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -17,19 +18,19 @@ import {
GithubLoginButton, GithubLoginButton,
EmailCodeForm, EmailCodeForm,
EmailPasswordForm, EmailPasswordForm,
EmailResetPasswordForm,
} from "components/account"; } from "components/account";
// ui // ui
import { Spinner } from "components/ui"; import { Spinner } from "components/ui";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// next themes // types
import { useTheme } from "next-themes";
import { IUser } from "types"; import { IUser } from "types";
const appConfig = new AppConfigService();
// types // types
type EmailPasswordFormValues = { type EmailPasswordFormValues = {
email: string; email: string;
@ -39,11 +40,16 @@ type EmailPasswordFormValues = {
const HomePage: NextPage = observer(() => { const HomePage: NextPage = observer(() => {
const store: any = useMobxStore(); const store: any = useMobxStore();
// theme
const { setTheme } = useTheme(); const { setTheme } = useTheme();
// user
const { isLoading, mutateUser } = useUserAuth("sign-in"); const { isLoading, mutateUser } = useUserAuth("sign-in");
// states
const [isResettingPassword, setIsResettingPassword] = useState(false);
// toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// fetch app config
const { data } = useSWR("APP_CONFIG", () => appConfig.envConfig());
const handleTheme = (user: IUser) => { const handleTheme = (user: IUser) => {
const currentTheme = user.theme.theme ?? "system"; const currentTheme = user.theme.theme ?? "system";
@ -79,11 +85,11 @@ const HomePage: NextPage = observer(() => {
const handleGitHubSignIn = async (credential: string) => { const handleGitHubSignIn = async (credential: string) => {
try { try {
if (process.env.NEXT_PUBLIC_GITHUB_ID && credential) { if (data && data.github && credential) {
const socialAuthPayload = { const socialAuthPayload = {
medium: "github", medium: "github",
credential, credential,
clientId: process.env.NEXT_PUBLIC_GITHUB_ID, clientId: data.github,
}; };
const response = await authenticationService.socialAuth(socialAuthPayload); const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) { if (response && response?.user) {
@ -149,10 +155,6 @@ const HomePage: NextPage = observer(() => {
} }
}; };
useEffect(() => {
setTheme("system");
}, [setTheme]);
return ( return (
<DefaultLayout> <DefaultLayout>
{isLoading ? ( {isLoading ? (
@ -173,38 +175,54 @@ const HomePage: NextPage = observer(() => {
</> </>
<div className="grid place-items-center h-full overflow-y-auto py-5 px-7"> <div className="grid place-items-center h-full overflow-y-auto py-5 px-7">
<div> <div>
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? ( <h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
{isResettingPassword ? "Reset your password" : "Sign in to Plane"}
</h1>
{isResettingPassword ? (
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
) : (
<> <>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100"> {data?.email_password_login && (
Sign in to Plane <EmailPasswordForm
</h1> onSubmit={handlePasswordSignIn}
<div className="flex flex-col divide-y divide-custom-border-200"> setIsResettingPassword={setIsResettingPassword}
<div className="pb-7"> />
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} /> )}
</div> {data?.magic_login && (
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden"> <div className="flex flex-col divide-y divide-custom-border-200">
<GoogleLoginButton handleSignIn={handleGoogleSignIn} /> <div className="pb-7">
<GithubLoginButton handleSignIn={handleGitHubSignIn} /> <EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
</div>
</div> </div>
)}
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
{data?.google && (
<GoogleLoginButton
clientId={data?.google}
handleSignIn={handleGoogleSignIn}
/>
)}
{data?.github && (
<GithubLoginButton
clientId={data?.github}
handleSignIn={handleGitHubSignIn}
/>
)}
</div> </div>
</> </>
) : (
<EmailPasswordForm onSubmit={handlePasswordSignIn} />
)} )}
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? ( <p className="pt-16 text-custom-text-200 text-sm text-center">
<p className="pt-16 text-custom-text-200 text-sm text-center"> By signing up, you agree to the{" "}
By signing up, you agree to the{" "} <a
<a href="https://plane.so/terms-and-conditions"
href="https://plane.so/terms-and-conditions" target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" className="font-medium underline"
className="font-medium underline" >
> Terms & Conditions
Terms & Conditions </a>
</a> </p>
</p>
) : null}
</div> </div>
</div> </div>
</> </>

View File

@ -1,8 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect } from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// next-themes // next-themes
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// services // services
@ -13,9 +11,7 @@ import useToast from "hooks/use-toast";
// layouts // layouts
import DefaultLayout from "layouts/default-layout"; import DefaultLayout from "layouts/default-layout";
// components // components
import { EmailPasswordForm } from "components/account"; import { EmailPasswordForm, EmailSignUpForm } from "components/account";
// ui
import { Spinner } from "components/ui";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// types // types
@ -27,8 +23,6 @@ type EmailPasswordFormValues = {
}; };
const SignUp: NextPage = () => { const SignUp: NextPage = () => {
const [isLoading, setIsLoading] = useState(true);
const router = useRouter(); const router = useRouter();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -70,18 +64,6 @@ const SignUp: NextPage = () => {
setTheme("system"); setTheme("system");
}, [setTheme]); }, [setTheme]);
useEffect(() => {
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0")) router.push("/");
else setIsLoading(false);
}, [router]);
if (isLoading)
return (
<div className="grid place-items-center h-screen w-full">
<Spinner />
</div>
);
return ( return (
<DefaultLayout> <DefaultLayout>
<> <>
@ -96,7 +78,8 @@ const SignUp: NextPage = () => {
</> </>
<div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7"> <div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7">
<div> <div>
<EmailPasswordForm onSubmit={handleSignUp} /> <h1 className="text-2xl text-center font-">SignUp on Plane</h1>
<EmailSignUpForm onSubmit={handleSignUp} />
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>

View File

@ -0,0 +1,30 @@
// services
import APIService from "services/api.service";
// helper
import { API_BASE_URL } from "helpers/common.helper";
export interface IEnvConfig {
github: string;
google: string;
github_app_name: string | null;
email_password_login: boolean;
magic_login: boolean;
}
export class AppConfigService extends APIService {
constructor() {
super(API_BASE_URL);
}
async envConfig(): Promise<IEnvConfig> {
return this.get("/api/configs/", {
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}

View File

@ -76,21 +76,23 @@ class FileServices extends APIService {
}); });
} }
async getUnsplashImages(page: number = 1, query?: string): Promise<UnSplashImage[]> { async getUnsplashImages(query?: string): Promise<UnSplashImage[]> {
const url = "/api/unsplash"; return this.get(`/api/unsplash/`, {
return this.request({
method: "get",
url,
params: { params: {
page,
per_page: 20,
query, query,
}, },
}) })
.then((response) => response?.data?.results ?? response?.data) .then((res) => res?.data?.results ?? res?.data)
.catch((error) => { .catch((err) => {
throw error?.response?.data; throw err?.response?.data;
});
}
async getProjectCoverImages(): Promise<string[]> {
return this.get(`/api/project-covers/`)
.then((res) => res?.data)
.catch((err) => {
throw err?.response?.data;
}); });
} }
} }