diff --git a/admin/package.json b/admin/package.json index 713a83e57..8d20fd5c4 100644 --- a/admin/package.json +++ b/admin/package.json @@ -14,6 +14,7 @@ "@headlessui/react": "^1.7.19", "@plane/types": "*", "@plane/ui": "*", + "@plane/constants": "*", "@tailwindcss/typography": "^0.5.9", "@types/lodash": "^4.17.0", "autoprefixer": "10.4.14", diff --git a/package.json b/package.json index 05c1c7f24..a63bbf340 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "packages/tailwind-config-custom", "packages/tsconfig", "packages/ui", - "packages/types" + "packages/types", + "packages/constants" ], "scripts": { "build": "turbo run build", @@ -25,7 +26,7 @@ "devDependencies": { "autoprefixer": "^10.4.15", "eslint-config-custom": "*", - "postcss": "^8.4.38", + "postcss": "^8.4.29", "prettier": "latest", "prettier-plugin-tailwindcss": "^0.5.4", "tailwindcss": "^3.3.3", diff --git a/packages/constants/package.json b/packages/constants/package.json new file mode 100644 index 000000000..be581d08a --- /dev/null +++ b/packages/constants/package.json @@ -0,0 +1,10 @@ +{ + "name": "@plane/constants", + "version": "0.0.1", + "private": true, + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*" + } +} diff --git a/packages/constants/src/auth.ts b/packages/constants/src/auth.ts new file mode 100644 index 000000000..a7e467153 --- /dev/null +++ b/packages/constants/src/auth.ts @@ -0,0 +1,360 @@ +import { ReactNode } from "react"; +import Link from "next/link"; + +export enum EPageTypes { + PUBLIC = "PUBLIC", + NON_AUTHENTICATED = "NON_AUTHENTICATED", + SET_PASSWORD = "SET_PASSWORD", + ONBOARDING = "ONBOARDING", + AUTHENTICATED = "AUTHENTICATED", +} + +export enum EAuthModes { + SIGN_IN = "SIGN_IN", + SIGN_UP = "SIGN_UP", +} + +export enum EAuthSteps { + EMAIL = "EMAIL", + PASSWORD = "PASSWORD", + UNIQUE_CODE = "UNIQUE_CODE", +} + +// TODO: remove this +export enum EErrorAlertType { + BANNER_ALERT = "BANNER_ALERT", + INLINE_FIRST_NAME = "INLINE_FIRST_NAME", + INLINE_EMAIL = "INLINE_EMAIL", + INLINE_PASSWORD = "INLINE_PASSWORD", + INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE", +} + +export enum EAuthErrorCodes { + // Global + INSTANCE_NOT_CONFIGURED = "5000", + INVALID_EMAIL = "5005", + EMAIL_REQUIRED = "5010", + SIGNUP_DISABLED = "5015", + MAGIC_LINK_LOGIN_DISABLED = "5017", + PASSWORD_LOGIN_DISABLED = "5019", + SMTP_NOT_CONFIGURED = "5025", + // Password strength + INVALID_PASSWORD = "5020", + // Sign Up + USER_ALREADY_EXIST = "5030", + AUTHENTICATION_FAILED_SIGN_UP = "5035", + REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5040", + INVALID_EMAIL_SIGN_UP = "5045", + INVALID_EMAIL_MAGIC_SIGN_UP = "5050", + MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055", + // Sign In + USER_DOES_NOT_EXIST = "5060", + AUTHENTICATION_FAILED_SIGN_IN = "5065", + REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070", + INVALID_EMAIL_SIGN_IN = "5075", + INVALID_EMAIL_MAGIC_SIGN_IN = "5080", + MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED = "5085", + // Both Sign in and Sign up for magic + INVALID_MAGIC_CODE = "5090", + EXPIRED_MAGIC_CODE = "5095", + EMAIL_CODE_ATTEMPT_EXHAUSTED = "5100", + // Oauth + GOOGLE_NOT_CONFIGURED = "5105", + GITHUB_NOT_CONFIGURED = "5110", + GOOGLE_OAUTH_PROVIDER_ERROR = "5115", + GITHUB_OAUTH_PROVIDER_ERROR = "5120", + // Reset Password + INVALID_PASSWORD_TOKEN = "5125", + EXPIRED_PASSWORD_TOKEN = "5130", + // Change password + INCORRECT_OLD_PASSWORD = "5135", + INVALID_NEW_PASSWORD = "5140", + // set passowrd + PASSWORD_ALREADY_SET = "5145", + // Admin + ADMIN_ALREADY_EXIST = "5150", + REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155", + INVALID_ADMIN_EMAIL = "5160", + INVALID_ADMIN_PASSWORD = "5165", + REQUIRED_ADMIN_EMAIL_PASSWORD = "5170", + ADMIN_AUTHENTICATION_FAILED = "5175", + ADMIN_USER_ALREADY_EXIST = "5180", + ADMIN_USER_DOES_NOT_EXIST = "5185", +} + +export type TAuthErrorInfo = { + type: EErrorAlertType; + code: EAuthErrorCodes; + title: string; + message: ReactNode; +}; + +const errorCodeMessages: { + [key in EAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode }; +} = { + // global + [EAuthErrorCodes.INSTANCE_NOT_CONFIGURED]: { + title: `Instance not configured`, + message: () => `Instance not configured. Please contact your administrator.`, + }, + [EAuthErrorCodes.SIGNUP_DISABLED]: { + title: `Sign up disabled`, + message: () => `Sign up disabled. Please contact your administrator.`, + }, + [EAuthErrorCodes.INVALID_PASSWORD]: { + title: `Invalid password`, + message: () => `Invalid password. Please try again.`, + }, + [EAuthErrorCodes.SMTP_NOT_CONFIGURED]: { + title: `SMTP not configured`, + message: () => `SMTP not configured. Please contact your administrator.`, + }, + + // email check in both sign up and sign in + [EAuthErrorCodes.INVALID_EMAIL]: { + title: `Invalid email`, + message: () => `Invalid email. Please try again.`, + }, + [EAuthenticationErrorCodes.EMAIL_REQUIRED]: { + title: `Email required`, + message: () => `Email required. Please try again.`, + }, + + // sign up + [EAuthenticationErrorCodes.USER_ALREADY_EXIST]: { + title: `User already exists`, + message: (email = undefined) => ( +
+ Your account is already registered.  + + Sign In + +  now. +
+ ), + }, + [EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: { + title: `Email and password required`, + message: () => `Email and password required. Please try again.`, + }, + [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP]: { + title: `Authentication failed`, + message: () => `Authentication failed. Please try again.`, + }, + [EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP]: { + title: `Invalid email`, + message: () => `Invalid email. Please try again.`, + }, + [EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED]: { + title: `Email and code required`, + message: () => `Email and code required. Please try again.`, + }, + [EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: { + title: `Invalid email`, + message: () => `Invalid email. Please try again.`, + }, + + // sign in + [EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: { + title: `User does not exist`, + message: (email = undefined) => ( +
+ No account found.  + + Create one + +  to get started. +
+ ), + }, + [EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN]: { + title: `Email and password required`, + message: () => `Email and password required. Please try again.`, + }, + [EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]: { + title: `Authentication failed`, + message: () => `Authentication failed. Please try again.`, + }, + [EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN]: { + title: `Invalid email`, + message: () => `Invalid email. Please try again.`, + }, + [EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED]: { + title: `Email and code required`, + message: () => `Email and code required. Please try again.`, + }, + [EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN]: { + title: `Invalid email`, + message: () => `Invalid email. Please try again.`, + }, + + // Both Sign in and Sign up + [EAuthenticationErrorCodes.INVALID_MAGIC_CODE]: { + title: `Authentication failed`, + message: () => `Invalid magic code. Please try again.`, + }, + [EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE]: { + title: `Expired magic code`, + message: () => `Expired magic code. Please try again.`, + }, + [EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED]: { + title: `Expired magic code`, + message: () => `Expired magic code. Please try again.`, + }, + + // Oauth + [EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: { + title: `Google not configured`, + message: () => `Google not configured. Please contact your administrator.`, + }, + [EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED]: { + title: `GitHub not configured`, + message: () => `GitHub not configured. Please contact your administrator.`, + }, + [EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: { + title: `Google OAuth provider error`, + message: () => `Google OAuth provider error. Please try again.`, + }, + [EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR]: { + title: `GitHub OAuth provider error`, + message: () => `GitHub OAuth provider error. Please try again.`, + }, + + // Reset Password + [EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: { + title: `Invalid password token`, + message: () => `Invalid password token. Please try again.`, + }, + [EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN]: { + title: `Expired password token`, + message: () => `Expired password token. Please try again.`, + }, + + // Change password + [EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: { + title: `Incorrect old password`, + message: () => `Incorrect old password. Please try again.`, + }, + [EAuthenticationErrorCodes.INVALID_NEW_PASSWORD]: { + title: `Invalid new password`, + message: () => `Invalid new password. Please try again.`, + }, + + // set password + [EAuthenticationErrorCodes.PASSWORD_ALREADY_SET]: { + title: `Password already set`, + message: () => `Password already set. Please try again.`, + }, + + // admin + [EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: { + title: `Admin already exists`, + message: () => `Admin already exists. Please try again.`, + }, + [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: { + title: `Email, password and first name required`, + message: () => `Email, password and first name required. Please try again.`, + }, + [EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: { + title: `Invalid admin email`, + message: () => `Invalid admin email. Please try again.`, + }, + [EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: { + title: `Invalid admin password`, + message: () => `Invalid admin password. Please try again.`, + }, + [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: { + title: `Email and password required`, + message: () => `Email and password required. Please try again.`, + }, + [EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: { + title: `Authentication failed`, + message: () => `Authentication failed. Please try again.`, + }, + [EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: { + title: `Admin user already exists`, + message: () => ( +
+ Admin user already exists.  + + Sign In + +  now. +
+ ), + }, + [EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: { + title: `Admin user does not exist`, + message: () => ( +
+ Admin user does not exist.  + + Sign In + +  now. +
+ ), + }, +}; + +export const authErrorHandler = ( + errorCode: EAuthenticationErrorCodes, + email?: string | undefined +): TAuthErrorInfo | undefined => { + const bannerAlertErrorCodes = [ + EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED, + EAuthenticationErrorCodes.INVALID_EMAIL, + EAuthenticationErrorCodes.EMAIL_REQUIRED, + EAuthenticationErrorCodes.SIGNUP_DISABLED, + EAuthenticationErrorCodes.INVALID_PASSWORD, + EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED, + EAuthenticationErrorCodes.USER_ALREADY_EXIST, + EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP, + EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP, + EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP, + EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP, + EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED, + EAuthenticationErrorCodes.USER_DOES_NOT_EXIST, + EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN, + EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN, + EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN, + EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN, + EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED, + EAuthenticationErrorCodes.INVALID_MAGIC_CODE, + EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE, + EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED, + EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED, + EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED, + EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR, + EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR, + EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN, + EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN, + EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD, + EAuthenticationErrorCodes.INVALID_NEW_PASSWORD, + EAuthenticationErrorCodes.PASSWORD_ALREADY_SET, + EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST, + EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME, + EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL, + EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD, + EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD, + EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED, + EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST, + EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST, + ]; + + if (bannerAlertErrorCodes.includes(errorCode)) + return { + type: EErrorAlertType.BANNER_ALERT, + code: errorCode, + title: errorCodeMessages[errorCode]?.title || "Error", + message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.", + }; + + return undefined; +}; diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts new file mode 100644 index 000000000..97ccf7649 --- /dev/null +++ b/packages/constants/src/index.ts @@ -0,0 +1 @@ +export * from "./auth"; diff --git a/packages/eslint-config-custom/index.js b/packages/eslint-config-custom/index.js index 9eae2e3f9..39d657d93 100644 --- a/packages/eslint-config-custom/index.js +++ b/packages/eslint-config-custom/index.js @@ -13,7 +13,7 @@ module.exports = { plugins: ["react", "@typescript-eslint", "import"], settings: { next: { - rootDir: ["web/", "space/", "packages/*/"], + rootDir: ["web/", "space/", "admin/", "packages/*/"], }, }, rules: { diff --git a/space/app/[workspace_slug]/[project_id]/error.tsx b/space/app/[workspace_slug]/[project_id]/error.tsx new file mode 100644 index 000000000..b666762d1 --- /dev/null +++ b/space/app/[workspace_slug]/[project_id]/error.tsx @@ -0,0 +1,5 @@ +"use client"; + +export default function ProjectError() { + return <>Project Error; +} diff --git a/space/app/[workspace_slug]/[project_id]/layout.tsx b/space/app/[workspace_slug]/[project_id]/layout.tsx new file mode 100644 index 000000000..fdc85700c --- /dev/null +++ b/space/app/[workspace_slug]/[project_id]/layout.tsx @@ -0,0 +1,38 @@ +import Image from "next/image"; +// components +import IssueNavbar from "@/components/issues/navbar"; +// hooks +import { useProject } from "@/hooks/store"; +// services +import ProjectService from "@/services/project.service"; +// assets +import planeLogo from "public/plane-logo.svg"; + +const projectService = new ProjectService(); + +export default async function ProjectLayout({ children, params }: { children: React.ReactNode; params: any }) { + const { workspace_slug, project_id } = params; + const projectSettings = await projectService.getProjectSettings(workspace_slug, project_id); + + return ( +
+
+ +
+
{children}
+ +
+ Plane logo +
+
+ Powered by Plane Deploy +
+
+
+ ); +} diff --git a/space/app/[workspace_slug]/[project_id]/page.tsx b/space/app/[workspace_slug]/[project_id]/page.tsx new file mode 100644 index 000000000..ee262cb38 --- /dev/null +++ b/space/app/[workspace_slug]/[project_id]/page.tsx @@ -0,0 +1,8 @@ +// components +import { ProjectDetailsView } from "@/components/views"; + +export default async function WorkspaceProjectPage({ params }: { params: any }) { + const { workspace_slug, project_id, ...rest } = params; + + return ; +} diff --git a/space/app/error.tsx b/space/app/error.tsx new file mode 100644 index 000000000..0c1e9d907 --- /dev/null +++ b/space/app/error.tsx @@ -0,0 +1,5 @@ +"use client"; + +export default function InstanceError() { + return
Instance Error
; +} diff --git a/space/app/layout.tsx b/space/app/layout.tsx new file mode 100644 index 000000000..24e23a372 --- /dev/null +++ b/space/app/layout.tsx @@ -0,0 +1,49 @@ +import { Metadata } from "next"; +// styles +import "@/styles/globals.css"; +// components +import { InstanceNotReady } from "@/components/instance"; +// lib +import { AppProvider } from "@/lib/app-providers"; +// services +import { InstanceService } from "@/services/instance.service"; + +const instanceService = new InstanceService(); + +export const metadata: Metadata = { + title: "Plane Deploy | Make your Plane boards public with one-click", + description: "Plane Deploy is a customer feedback management tool built on top of plane.so", + openGraph: { + title: "Plane Deploy | Make your Plane boards public with one-click", + description: "Plane Deploy is a customer feedback management tool built on top of plane.so", + url: "https://sites.plane.so/", + }, + keywords: + "software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration", + twitter: { + site: "@planepowers", + }, +}; + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const instanceDetails = await instanceService.getInstanceInfo(); + + return ( + + + {/* + + + + */} + + + {!instanceDetails?.instance?.is_setup_done ? ( + + ) : ( + {children} + )} + + + ); +} diff --git a/space/app/page.tsx b/space/app/page.tsx new file mode 100644 index 000000000..bdd42c3af --- /dev/null +++ b/space/app/page.tsx @@ -0,0 +1,31 @@ +// components +import { UserLoggedIn } from "@/components/accounts"; +import { AuthView } from "@/components/views"; +// helpers +// import { EPageTypes } from "@/helpers/authentication.helper"; +// import { useInstance, useUser } from "@/hooks/store"; +// wrapper +// import { AuthWrapper } from "@/lib/wrappers"; +// services +import { UserService } from "@/services/user.service"; + +const userServices = new UserService(); + +export default async function HomePage() { + const user = await userServices + .currentUser() + .then((user) => ({ ...user, isAuthenticated: true })) + .catch(() => ({ isAuthenticated: false })); + + // const { data } = useInstance(); + + // console.log("data", data); + console.log("user", user); + + if (user.isAuthenticated) { + return ; + } + + // return <>Login View; + return ; +} diff --git a/space/components/accounts/auth-forms/email.tsx b/space/components/accounts/auth-forms/email.tsx index 550dea2bf..8f40b74d5 100644 --- a/space/components/accounts/auth-forms/email.tsx +++ b/space/components/accounts/auth-forms/email.tsx @@ -1,3 +1,5 @@ +"use client"; + import React from "react"; import { Controller, useForm } from "react-hook-form"; // icons diff --git a/space/components/accounts/auth-forms/forgot-password-popover.tsx b/space/components/accounts/auth-forms/forgot-password-popover.tsx index 31bafce26..8e0f2064c 100644 --- a/space/components/accounts/auth-forms/forgot-password-popover.tsx +++ b/space/components/accounts/auth-forms/forgot-password-popover.tsx @@ -1,3 +1,4 @@ +"use client"; import { Fragment, useState } from "react"; import { usePopper } from "react-popper"; import { X } from "lucide-react"; diff --git a/space/components/accounts/auth-forms/password.tsx b/space/components/accounts/auth-forms/password.tsx index 35d8703b6..bd64f24d1 100644 --- a/space/components/accounts/auth-forms/password.tsx +++ b/space/components/accounts/auth-forms/password.tsx @@ -1,7 +1,9 @@ +"use client"; + import React, { useEffect, useMemo, useState } from "react"; // icons import Link from "next/link"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { Eye, EyeOff, XCircle } from "lucide-react"; // ui import { Button, Input, Spinner } from "@plane/ui"; @@ -12,7 +14,7 @@ import { API_BASE_URL } from "@/helpers/common.helper"; import { getPasswordStrength } from "@/helpers/password.helper"; // hooks import { useInstance } from "@/hooks/store"; -import { AuthService } from "@/services/authentication.service"; +import { AuthService } from "@/services/auth.service"; type Props = { email: string; @@ -43,12 +45,12 @@ export const PasswordForm: React.FC = (props) => { const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); // hooks - const { instance } = useInstance(); + const { data: instance, config: instanceConfig } = useInstance(); // router const router = useRouter(); const { next_path } = router.query; // derived values - const isSmtpConfigured = instance?.config?.is_smtp_configured; + const isSmtpConfigured = instanceConfig?.is_smtp_configured; const handleFormChange = (key: keyof TPasswordFormValues, value: string) => setPasswordFormData((prev) => ({ ...prev, [key]: value })); diff --git a/space/components/accounts/auth-forms/root.tsx b/space/components/accounts/auth-forms/root.tsx index 1dac88655..273a438bf 100644 --- a/space/components/accounts/auth-forms/root.tsx +++ b/space/components/accounts/auth-forms/root.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components @@ -7,7 +9,7 @@ import { EmailForm, UniqueCodeForm, PasswordForm, OAuthOptions, TermsAndConditio import { useInstance } from "@/hooks/store"; import useToast from "@/hooks/use-toast"; // services -import { AuthService } from "@/services/authentication.service"; +import { AuthService } from "@/services/auth.service"; export enum EAuthSteps { EMAIL = "EMAIL", @@ -60,9 +62,9 @@ export const AuthRoot = observer(() => { const [authStep, setAuthStep] = useState(EAuthSteps.EMAIL); const [email, setEmail] = useState(""); // hooks - const { instance } = useInstance(); + const { config: instanceConfig } = useInstance(); // derived values - const isSmtpConfigured = instance?.config?.is_smtp_configured; + const isSmtpConfigured = instanceConfig?.is_smtp_configured; const { header, subHeader } = getHeaderSubHeader(authMode); @@ -112,8 +114,8 @@ export const AuthRoot = observer(() => { ); }; - const isOAuthEnabled = - instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled); + const isOAuthEnabled = instanceConfig && (instanceConfig?.is_google_enabled || instanceConfig?.is_github_enabled); + return (
@@ -149,7 +151,7 @@ export const AuthRoot = observer(() => { )} )} - {isOAuthEnabled && } + {isOAuthEnabled !== undefined && }
); diff --git a/space/components/accounts/auth-forms/unique-code.tsx b/space/components/accounts/auth-forms/unique-code.tsx index bf76acdb9..bcb696a1f 100644 --- a/space/components/accounts/auth-forms/unique-code.tsx +++ b/space/components/accounts/auth-forms/unique-code.tsx @@ -1,5 +1,7 @@ +"use client"; + import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; // icons import { CircleCheck, XCircle } from "lucide-react"; // ui @@ -10,7 +12,7 @@ import { API_BASE_URL } from "@/helpers/common.helper"; import useTimer from "@/hooks/use-timer"; import useToast from "@/hooks/use-toast"; // services -import { AuthService } from "@/services/authentication.service"; +import { AuthService } from "@/services/auth.service"; // types import { IEmailCheckData } from "@/types/auth"; import { EAuthModes } from "./root"; diff --git a/space/components/accounts/oauth/github-button.tsx b/space/components/accounts/oauth/github-button.tsx index 740d59aaf..9a907f03a 100644 --- a/space/components/accounts/oauth/github-button.tsx +++ b/space/components/accounts/oauth/github-button.tsx @@ -1,3 +1,5 @@ +"use client"; + import { FC } from "react"; import Image from "next/image"; import { useTheme } from "next-themes"; diff --git a/space/components/accounts/oauth/google-button.tsx b/space/components/accounts/oauth/google-button.tsx index d31c6f59f..035a541a5 100644 --- a/space/components/accounts/oauth/google-button.tsx +++ b/space/components/accounts/oauth/google-button.tsx @@ -1,3 +1,5 @@ +"use client"; + import { FC } from "react"; import Image from "next/image"; import { useTheme } from "next-themes"; diff --git a/space/components/accounts/oauth/oauth-options.tsx b/space/components/accounts/oauth/oauth-options.tsx index b8e86c2ca..13b8c7d27 100644 --- a/space/components/accounts/oauth/oauth-options.tsx +++ b/space/components/accounts/oauth/oauth-options.tsx @@ -1,3 +1,5 @@ +"use client"; + import { observer } from "mobx-react-lite"; // components import { GithubOAuthButton, GoogleOAuthButton } from "@/components/accounts"; @@ -6,7 +8,7 @@ import { useInstance } from "@/hooks/store"; export const OAuthOptions: React.FC = observer(() => { // hooks - const { instance } = useInstance(); + const { config: instanceConfig } = useInstance(); return ( <> @@ -16,12 +18,12 @@ export const OAuthOptions: React.FC = observer(() => {
- {instance?.config?.is_google_enabled && ( + {instanceConfig?.is_google_enabled && (
)} - {instance?.config?.is_github_enabled && } + {instanceConfig?.is_github_enabled && }
); diff --git a/space/components/accounts/onboarding-form.tsx b/space/components/accounts/onboarding-form.tsx index 768d5160f..50e5f0f0d 100644 --- a/space/components/accounts/onboarding-form.tsx +++ b/space/components/accounts/onboarding-form.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; @@ -8,7 +10,7 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // components import { UserImageUploadModal } from "@/components/accounts"; // hooks -import { useMobxStore } from "@/hooks/store"; +import { useUser } from "@/hooks/store"; // services import fileService from "@/services/file.service"; @@ -35,9 +37,7 @@ export const OnBoardingForm: React.FC = observer((props) => { const [isRemoving, setIsRemoving] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); // store hooks - const { - user: { updateCurrentUser }, - } = useMobxStore(); + const { updateCurrentUser } = useUser(); // form info const { getValues, diff --git a/space/components/accounts/password-strength-meter.tsx b/space/components/accounts/password-strength-meter.tsx index 86ee814c8..c12d78421 100644 --- a/space/components/accounts/password-strength-meter.tsx +++ b/space/components/accounts/password-strength-meter.tsx @@ -1,3 +1,5 @@ +"use client"; + // icons import { CircleCheck } from "lucide-react"; // helpers diff --git a/space/components/accounts/terms-and-conditions.tsx b/space/components/accounts/terms-and-conditions.tsx index fbb16fd21..4bbde51f3 100644 --- a/space/components/accounts/terms-and-conditions.tsx +++ b/space/components/accounts/terms-and-conditions.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { FC } from "react"; import Link from "next/link"; import { EAuthModes } from "./auth-forms"; diff --git a/space/components/accounts/user-image-upload-modal.tsx b/space/components/accounts/user-image-upload-modal.tsx index fc0f4393a..802a6af62 100644 --- a/space/components/accounts/user-image-upload-modal.tsx +++ b/space/components/accounts/user-image-upload-modal.tsx @@ -1,3 +1,4 @@ +"use client"; import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; @@ -27,7 +28,7 @@ export const UserImageUploadModal: React.FC = observer((props) => { const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); // store hooks - const { instance } = useInstance(); + const { config: instanceConfig } = useInstance(); const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]); @@ -36,7 +37,7 @@ export const UserImageUploadModal: React.FC = observer((props) => { accept: { "image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"], }, - maxSize: instance?.config?.file_size_limit ?? MAX_FILE_SIZE, + maxSize: (instanceConfig?.file_size_limit as number) ?? MAX_FILE_SIZE, multiple: false, }); diff --git a/space/components/accounts/user-logged-in.tsx b/space/components/accounts/user-logged-in.tsx index 7bf864431..773d934ad 100644 --- a/space/components/accounts/user-logged-in.tsx +++ b/space/components/accounts/user-logged-in.tsx @@ -1,3 +1,5 @@ +"use client"; + import Image from "next/image"; // hooks import { useUser } from "@/hooks/store"; diff --git a/space/components/instance/not-ready-view.tsx b/space/components/instance/not-ready-view.tsx index 815e0d1fe..5b7944671 100644 --- a/space/components/instance/not-ready-view.tsx +++ b/space/components/instance/not-ready-view.tsx @@ -1,6 +1,7 @@ +"use client"; + import { FC } from "react"; import Image from "next/image"; -import Link from "next/link"; // ui import { Button } from "@plane/ui"; // helpers @@ -9,10 +10,10 @@ import { ADMIN_BASE_URL, ADMIN_BASE_PATH } from "@/helpers/common.helper"; import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png"; export const InstanceNotReady: FC = () => { - const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH + "/setup/?auth_enabled=0"); + const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH); return ( -
+

Welcome aboard Plane!

@@ -21,13 +22,12 @@ export const InstanceNotReady: FC = () => { Get started by setting up your instance and workspace

-
diff --git a/space/components/issues/board-views/kanban/block.tsx b/space/components/issues/board-views/kanban/block.tsx index 6c2aa5279..24e6f1fb1 100644 --- a/space/components/issues/board-views/kanban/block.tsx +++ b/space/components/issues/board-views/kanban/block.tsx @@ -2,7 +2,7 @@ // mobx react lite import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date"; import { IssueBlockPriority } from "@/components/issues/board-views/block-priority"; import { IssueBlockState } from "@/components/issues/board-views/block-state"; diff --git a/space/components/issues/board-views/list/block.tsx b/space/components/issues/board-views/list/block.tsx index 63b589066..60ecf53a1 100644 --- a/space/components/issues/board-views/list/block.tsx +++ b/space/components/issues/board-views/list/block.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; // components import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date"; import { IssueBlockLabels } from "@/components/issues/board-views/block-labels"; diff --git a/space/components/issues/board-views/list/index.tsx b/space/components/issues/board-views/list/index.tsx index 03ca07998..f5f5adb6b 100644 --- a/space/components/issues/board-views/list/index.tsx +++ b/space/components/issues/board-views/list/index.tsx @@ -2,27 +2,24 @@ import { observer } from "mobx-react-lite"; // components import { IssueListBlock } from "@/components/issues/board-views/list/block"; import { IssueListHeader } from "@/components/issues/board-views/list/header"; -// interfaces // mobx hook -import { useMobxStore } from "@/hooks/store"; -// store -import { RootStore } from "@/store/root.store"; -import { IIssueState, IIssue } from "types/issue"; +import { useIssue } from "@/hooks/store"; +// types +import { IIssueState, IIssue } from "@/types/issue"; export const IssueListView = observer(() => { - const { issue: issueStore }: RootStore = useMobxStore(); + const { states, getFilteredIssuesByState } = useIssue(); return ( <> - {issueStore?.states && - issueStore?.states.length > 0 && - issueStore?.states.map((_state: IIssueState) => ( + {states && + states.length > 0 && + states.map((_state: IIssueState) => (
- {issueStore.getFilteredIssuesByState(_state.id) && - issueStore.getFilteredIssuesByState(_state.id).length > 0 ? ( + {getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? (
- {issueStore.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( + {getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( ))}
diff --git a/space/components/issues/filters/applied-filters/root.tsx b/space/components/issues/filters/applied-filters/root.tsx index 7362e8787..1e32ea363 100644 --- a/space/components/issues/filters/applied-filters/root.tsx +++ b/space/components/issues/filters/applied-filters/root.tsx @@ -1,33 +1,31 @@ +"use client"; + import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; -// components +import { useRouter } from "next/navigation"; +// hooks +import { useIssue, useProject, useIssueFilter } from "@/hooks/store"; // store -import { useMobxStore } from "@/hooks/store"; -import { IIssueFilterOptions } from "@/store/issues/types"; -import { RootStore } from "@/store/root.store"; +import { IIssueFilterOptions } from "@/types/issue"; +// components import { AppliedFiltersList } from "./filters-list"; -export const IssueAppliedFilters: FC = observer(() => { +// TODO: fix component types +export const IssueAppliedFilters: FC = observer((props: any) => { const router = useRouter(); - const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as { - workspace_slug: string; - project_slug: string; - }; - - const { - issuesFilter: { issueFilters, updateFilters }, - issue: { states, labels }, - project: { activeBoard }, - }: RootStore = useMobxStore(); + const { workspaceSlug, projectId } = props; + const { states, labels } = useIssue(); + const { activeLayout } = useProject(); + const { issueFilters, updateFilters } = useIssueFilter(); const userFilters = issueFilters?.filters || {}; - const appliedFilters: IIssueFilterOptions = {}; + const appliedFilters: any = {}; + Object.entries(userFilters).forEach(([key, value]) => { if (!value) return; if (Array.isArray(value) && value.length === 0) return; - appliedFilters[key as keyof IIssueFilterOptions] = value; + appliedFilters[key] = value; }); const updateRouteParams = useCallback( @@ -36,16 +34,17 @@ export const IssueAppliedFilters: FC = observer(() => { const priority = key === "priority" ? value || [] : issueFilters?.filters?.priority ?? []; const labels = key === "labels" ? value || [] : issueFilters?.filters?.labels ?? []; - let params: any = { board: activeBoard || "list" }; + let params: any = { board: activeLayout || "list" }; if (!clearFields) { if (priority.length > 0) params = { ...params, priorities: priority.join(",") }; if (state.length > 0) params = { ...params, states: state.join(",") }; if (labels.length > 0) params = { ...params, labels: labels.join(",") }; } - - router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true }); + console.log("params", params); + // TODO: fix this redirection + // router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true }); }, - [workspaceSlug, projectId, activeBoard, issueFilters, router] + [workspaceSlug, projectId, activeLayout, issueFilters, router] ); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { @@ -80,7 +79,7 @@ export const IssueAppliedFilters: FC = observer(() => {
{ - const { - project: projectStore, - issuesFilter: { updateFilters }, - }: RootStore = useMobxStore(); - const { data: user } = useUser(); - // router +type IssueNavbarProps = { + projectSettings: any; +}; + +const IssueNavbar: FC = observer((props) => { + const { projectSettings } = props; + const { project_details, views } = projectSettings; + + console.log("projectSettings", projectSettings); + // hooks const router = useRouter(); - const { workspace_slug, project_slug, board, peekId, states, priorities, labels } = router.query as { - workspace_slug: string; - project_slug: string; - peekId: string; - board: string; - states: string; - priorities: string; - labels: string; - }; + // store + const { settings, activeLayout, hydrate } = useProject(); + hydrate(projectSettings); + const { data: user } = useUser(); + console.log("user", user); - useEffect(() => { - if (workspace_slug && project_slug) { - projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString()); - } - }, [projectStore, workspace_slug, project_slug]); + // return <>layout; - useEffect(() => { - if (workspace_slug && project_slug && projectStore?.deploySettings) { - const viewsAcceptable: string[] = []; - let currentBoard: TIssueBoardKeys | null = null; + // useEffect(() => { + // if (workspace_slug && project_slug && settings) { + // const viewsAcceptable: string[] = []; + // let currentBoard: TIssueBoardKeys | null = null; - if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("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"); - if (projectStore?.deploySettings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet"); + // if (settings?.views?.list) viewsAcceptable.push("list"); + // if (settings?.views?.kanban) viewsAcceptable.push("kanban"); + // if (settings?.views?.calendar) viewsAcceptable.push("calendar"); + // if (settings?.views?.gantt) viewsAcceptable.push("gantt"); + // if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet"); - if (board) { - if (viewsAcceptable.includes(board.toString())) { - currentBoard = board.toString() as TIssueBoardKeys; - } else { - if (viewsAcceptable && viewsAcceptable.length > 0) { - currentBoard = viewsAcceptable[0] as TIssueBoardKeys; - } - } - } else { - if (viewsAcceptable && viewsAcceptable.length > 0) { - currentBoard = viewsAcceptable[0] as TIssueBoardKeys; - } - } + // if (board) { + // if (viewsAcceptable.includes(board.toString())) { + // currentBoard = board.toString() as TIssueBoardKeys; + // } else { + // if (viewsAcceptable && viewsAcceptable.length > 0) { + // currentBoard = viewsAcceptable[0] as TIssueBoardKeys; + // } + // } + // } else { + // if (viewsAcceptable && viewsAcceptable.length > 0) { + // currentBoard = viewsAcceptable[0] as TIssueBoardKeys; + // } + // } - if (currentBoard) { - if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) { - let params: any = { board: currentBoard }; - if (peekId && peekId.length > 0) params = { ...params, peekId: peekId }; - if (priorities && priorities.length > 0) params = { ...params, priorities: priorities }; - if (states && states.length > 0) params = { ...params, states: states }; - if (labels && labels.length > 0) params = { ...params, labels: labels }; + // if (currentBoard) { + // if (projectStore?.layout === null || projectStore?.activeBoard !== currentBoard) { + // let params: any = { board: currentBoard }; + // if (peekId && peekId.length > 0) params = { ...params, peekId: peekId }; + // if (priorities && priorities.length > 0) params = { ...params, priorities: priorities }; + // if (states && states.length > 0) params = { ...params, states: states }; + // if (labels && labels.length > 0) params = { ...params, labels: labels }; - let storeParams: any = {}; - if (priorities && priorities.length > 0) storeParams = { ...storeParams, priority: priorities.split(",") }; - if (states && states.length > 0) storeParams = { ...storeParams, state: states.split(",") }; - if (labels && labels.length > 0) storeParams = { ...storeParams, labels: labels.split(",") }; + // let storeParams: any = {}; + // if (priorities && priorities.length > 0) storeParams = { ...storeParams, priority: priorities.split(",") }; + // if (states && states.length > 0) storeParams = { ...storeParams, state: states.split(",") }; + // if (labels && labels.length > 0) storeParams = { ...storeParams, labels: labels.split(",") }; - if (storeParams) updateFilters(project_slug, storeParams); + // if (storeParams) updateFilters(project_slug, storeParams); - projectStore.setActiveBoard(currentBoard); - router.push({ - pathname: `/${workspace_slug}/${project_slug}`, - query: { ...params }, - }); - } - } - } - }, [ - board, - workspace_slug, - project_slug, - router, - projectStore, - projectStore?.deploySettings, - updateFilters, - labels, - states, - priorities, - peekId, - ]); + // projectStore.setActiveBoard(currentBoard); + // router.push({ + // pathname: `/${workspace_slug}/${project_slug}`, + // query: { ...params }, + // }); + // } + // } + // } + // }, [ + // board, + // workspace_slug, + // project_slug, + // router, + // projectStore, + // projectStore?.deploySettings, + // updateFilters, + // labels, + // states, + // priorities, + // peekId, + // ]); return (
{/* project detail */}
- {projectStore.project ? ( + {project_details ? ( - + ) : ( @@ -115,16 +110,13 @@ const IssueNavbar = observer(() => { )}
- {projectStore?.project?.name || `...`} + {project_details?.name || `...`}
- {/* issue search bar */} -
{/* */}
- {/* issue views */}
- +
{/* issue filters */} @@ -137,7 +129,7 @@ const IssueNavbar = observer(() => {
- {user ? ( + {user?.id ? (
{user.display_name}
diff --git a/space/components/issues/navbar/issue-board-view.tsx b/space/components/issues/navbar/issue-board-view.tsx index 12574bef8..d2eb53398 100644 --- a/space/components/issues/navbar/issue-board-view.tsx +++ b/space/components/issues/navbar/issue-board-view.tsx @@ -1,47 +1,49 @@ +"use client"; + +import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // constants import { issueViews } from "@/constants/data"; +// hooks +import { useProject } from "@/hooks/store"; // mobx -import { useMobxStore } from "@/hooks/store"; -import { RootStore } from "@/store/root.store"; -import { TIssueBoardKeys } from "types/issue"; +import { TIssueBoardKeys } from "@/types/issue"; -export const NavbarIssueBoardView = observer(() => { - const { - project: { viewOptions, setActiveBoard, activeBoard }, - }: RootStore = useMobxStore(); - // router - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; +type NavbarIssueBoardViewProps = { + layouts: Record; +}; + +export const NavbarIssueBoardView: FC = observer((props) => { + const { layouts } = props; + + const { activeLayout, setActiveLayout } = useProject(); const handleCurrentBoardView = (boardView: string) => { - setActiveBoard(boardView as TIssueBoardKeys); - router.push(`/${workspace_slug}/${project_slug}?board=${boardView}`); + setActiveLayout(boardView as TIssueBoardKeys); }; return ( <> - {viewOptions && - Object.keys(viewOptions).map((viewKey: string) => { - if (viewOptions[viewKey]) { + {layouts && + Object.keys(layouts).map((layoutKey: string) => { + if (layouts[layoutKey as TIssueBoardKeys]) { return (
handleCurrentBoardView(viewKey)} - title={viewKey} + onClick={() => handleCurrentBoardView(layoutKey)} + title={layoutKey} > - {issueViews[viewKey]?.icon} + {issueViews[layoutKey]?.icon}
); diff --git a/space/components/issues/navbar/theme.tsx b/space/components/issues/navbar/theme.tsx index 1d45625c7..e09bdda60 100644 --- a/space/components/issues/navbar/theme.tsx +++ b/space/components/issues/navbar/theme.tsx @@ -1,3 +1,5 @@ +"use client"; + // next theme import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; @@ -16,7 +18,6 @@ export const NavbarTheme = observer(() => { useEffect(() => { if (!theme) return; - setAppTheme(theme); }, [theme]); diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index dadcc1747..878d2fab4 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -1,6 +1,6 @@ import React, { useRef } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { useForm, Controller } from "react-hook-form"; // components import { EditorRefApi } from "@plane/lite-text-editor"; diff --git a/space/components/issues/peek-overview/comment/comment-reactions.tsx b/space/components/issues/peek-overview/comment/comment-reactions.tsx index ca451f7b4..f6ce62fc4 100644 --- a/space/components/issues/peek-overview/comment/comment-reactions.tsx +++ b/space/components/issues/peek-overview/comment/comment-reactions.tsx @@ -1,6 +1,6 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { Tooltip } from "@plane/ui"; // ui import { ReactionSelector } from "@/components/ui"; diff --git a/space/components/issues/peek-overview/full-screen-peek-view.tsx b/space/components/issues/peek-overview/full-screen-peek-view.tsx index 5ab4dffd7..617bbc687 100644 --- a/space/components/issues/peek-overview/full-screen-peek-view.tsx +++ b/space/components/issues/peek-overview/full-screen-peek-view.tsx @@ -13,7 +13,7 @@ import { IIssue } from "@/types/issue"; type Props = { handleClose: () => void; issueDetails: IIssue | undefined; - workspace_slug: string; + workspaceSlug: string; }; export const FullScreenPeekView: React.FC = observer((props) => { diff --git a/space/components/issues/peek-overview/header.tsx b/space/components/issues/peek-overview/header.tsx index 7aba40305..17defbe36 100644 --- a/space/components/issues/peek-overview/header.tsx +++ b/space/components/issues/peek-overview/header.tsx @@ -9,7 +9,7 @@ import { Icon } from "@/components/ui"; import { copyTextToClipboard } from "@/helpers/string.helper"; // store import { useMobxStore } from "@/hooks/store"; -import { IPeekMode } from "@/store/issue_details"; +import { IPeekMode } from "@/store/issue-detail.store"; import { RootStore } from "@/store/root.store"; // lib import useToast from "hooks/use-toast"; diff --git a/space/components/issues/peek-overview/issue-activity.tsx b/space/components/issues/peek-overview/issue-activity.tsx index aaa7dc688..4fe50dc4a 100644 --- a/space/components/issues/peek-overview/issue-activity.tsx +++ b/space/components/issues/peek-overview/issue-activity.tsx @@ -1,7 +1,7 @@ import React from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { Button } from "@plane/ui"; // components import { CommentCard, AddComment } from "@/components/issues/peek-overview"; diff --git a/space/components/issues/peek-overview/issue-emoji-reactions.tsx b/space/components/issues/peek-overview/issue-emoji-reactions.tsx index 7e568461b..ef5688c57 100644 --- a/space/components/issues/peek-overview/issue-emoji-reactions.tsx +++ b/space/components/issues/peek-overview/issue-emoji-reactions.tsx @@ -1,20 +1,22 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // lib import { Tooltip } from "@plane/ui"; import { ReactionSelector } from "@/components/ui"; // helpers import { groupReactions, renderEmoji } from "@/helpers/emoji.helper"; // hooks -import { useMobxStore, useUser } from "@/hooks/store"; +import { useIssueDetails, useUser } from "@/hooks/store"; -export const IssueEmojiReactions: React.FC = observer(() => { - // router - const router = useRouter(); - const { workspace_slug, project_slug } = router.query; +type IssueEmojiReactionsProps = { + workspaceSlug: string; + projectId: string; +}; + +export const IssueEmojiReactions: React.FC = observer((props) => { + const { workspaceSlug, projectId } = props; // store - const { issueDetails: issueDetailsStore } = useMobxStore(); + const issueDetailsStore = useIssueDetails(); const { data: user, fetchCurrentUser } = useUser(); const issueId = issueDetailsStore.peekId; @@ -24,20 +26,17 @@ export const IssueEmojiReactions: React.FC = observer(() => { const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id); const handleAddReaction = (reactionHex: string) => { - if (!workspace_slug || !project_slug || !issueId) return; - - issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex); + if (!workspaceSlug || !projectId || !issueId) return; + issueDetailsStore.addIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex); }; const handleRemoveReaction = (reactionHex: string) => { - if (!workspace_slug || !project_slug || !issueId) return; - - issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex); + if (!workspaceSlug || !projectId || !issueId) return; + issueDetailsStore.removeIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex); }; const handleReactionClick = (reactionHex: string) => { const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex); - if (userReaction) handleRemoveReaction(reactionHex); else handleAddReaction(reactionHex); }; diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index 1018c22f7..d31e8dd6d 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -8,7 +8,7 @@ import { issueGroupFilter, issuePriorityFilter } from "@/constants/data"; import { renderFullDate } from "@/helpers/date-time.helper"; import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper"; // types -import { IPeekMode } from "@/store/issue_details"; +import { IPeekMode } from "@/store/issue-detail.store"; // constants import useToast from "hooks/use-toast"; import { IIssue } from "types/issue"; diff --git a/space/components/issues/peek-overview/issue-reaction.tsx b/space/components/issues/peek-overview/issue-reaction.tsx index 5bc60cb34..7daaa29e5 100644 --- a/space/components/issues/peek-overview/issue-reaction.tsx +++ b/space/components/issues/peek-overview/issue-reaction.tsx @@ -1,12 +1,18 @@ import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview"; -import { useMobxStore } from "@/hooks/store"; +import { useProject } from "@/hooks/store"; -export const IssueReactions: React.FC = () => { - const { project: projectStore } = useMobxStore(); +type IssueReactionsProps = { + workspaceSlug: string; + projectId: string; +}; + +export const IssueReactions: React.FC = (props) => { + const { workspaceSlug, projectId } = props; + const { canVote, canReact } = useProject(); return (
- {projectStore?.deploySettings?.votes && ( + {canVote && ( <>
@@ -14,9 +20,9 @@ export const IssueReactions: React.FC = () => {
)} - {projectStore?.deploySettings?.reactions && ( + {canReact && (
- +
)}
diff --git a/space/components/issues/peek-overview/issue-vote-reactions.tsx b/space/components/issues/peek-overview/issue-vote-reactions.tsx index 64568f66c..cab6f73ad 100644 --- a/space/components/issues/peek-overview/issue-vote-reactions.tsx +++ b/space/components/issues/peek-overview/issue-vote-reactions.tsx @@ -1,18 +1,17 @@ +"use client"; + import { useState, useEffect } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; import { Tooltip } from "@plane/ui"; // hooks -import { useMobxStore, useUser } from "@/hooks/store"; +import { useIssueDetails, useUser } from "@/hooks/store"; -export const IssueVotes: React.FC = observer(() => { +export const IssueVotes: React.FC = observer((props: any) => { + const { workspaceSlug, projectId } = props; + // states const [isSubmitting, setIsSubmitting] = useState(false); - const router = useRouter(); - - const { workspace_slug, project_slug } = router.query; - - const { issueDetails: issueDetailsStore } = useMobxStore(); + const issueDetailsStore = useIssueDetails(); const { data: user, fetchCurrentUser } = useUser(); const issueId = issueDetailsStore.peekId; @@ -26,16 +25,16 @@ export const IssueVotes: React.FC = observer(() => { const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id); const handleVote = async (e: any, voteValue: 1 | -1) => { - if (!workspace_slug || !project_slug || !issueId) return; + if (!workspaceSlug || !projectId || !issueId) return; setIsSubmitting(true); const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue); if (actionPerformed) - await issueDetailsStore.removeIssueVote(workspace_slug.toString(), project_slug.toString(), issueId); + await issueDetailsStore.removeIssueVote(workspaceSlug.toString(), projectId.toString(), issueId); else - await issueDetailsStore.addIssueVote(workspace_slug.toString(), project_slug.toString(), issueId, { + await issueDetailsStore.addIssueVote(workspaceSlug.toString(), projectId.toString(), issueId, { vote: voteValue, }); diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index 01183fb2d..f75f03f5f 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -1,42 +1,32 @@ +"use client"; + import React, { useEffect, useState } from "react"; - import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; - -// mobx // headless ui import { Dialog, Transition } from "@headlessui/react"; // components import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview"; -// lib -import { useMobxStore } from "@/hooks/store"; +// store +import { useIssue, useIssueDetails } from "@/hooks/store"; -export const IssuePeekOverview: React.FC = observer(() => { +export const IssuePeekOverview: React.FC = observer((props: any) => { + const { workspaceSlug, projectId, peekId, board, priorities, states, labels } = props; // states const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); - // router - const router = useRouter(); - const { workspace_slug, project_slug, peekId, board, priorities, states, labels } = router.query as { - workspace_slug: string; - project_slug: string; - peekId: string; - board: string; - priorities: string; - states: string; - labels: string; - }; // store - const { issueDetails: issueDetailStore, issue: issueStore } = useMobxStore(); + const issueDetailStore = useIssueDetails(); + const issueStore = useIssue(); + const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined; useEffect(() => { - if (workspace_slug && project_slug && peekId && issueStore.issues && issueStore.issues.length > 0) { + if (workspaceSlug && projectId && peekId && issueStore.issues && issueStore.issues.length > 0) { if (!issueDetails) { - issueDetailStore.fetchIssueDetails(workspace_slug.toString(), project_slug.toString(), peekId.toString()); + issueDetailStore.fetchIssueDetails(workspaceSlug.toString(), projectId.toString(), peekId.toString()); } } - }, [workspace_slug, project_slug, issueDetailStore, issueDetails, peekId, issueStore.issues]); + }, [workspaceSlug, projectId, issueDetailStore, issueDetails, peekId, issueStore.issues]); const handleClose = () => { issueDetailStore.setPeekId(null); @@ -45,10 +35,8 @@ export const IssuePeekOverview: React.FC = observer(() => { if (states && states.length > 0) params.states = states; if (priorities && priorities.length > 0) params.priorities = priorities; if (labels && labels.length > 0) params.labels = labels; - - router.replace({ pathname: `/${workspace_slug?.toString()}/${project_slug}`, query: { ...params } }, undefined, { - shallow: true, - }); + // TODO: fix this redirection + // router.push( encodeURI(`/${workspaceSlug?.toString()}/${projectId}`, ) { pathname: `/${workspaceSlug?.toString()}/${projectId}`, query: { ...params } }); }; useEffect(() => { @@ -118,7 +106,7 @@ export const IssuePeekOverview: React.FC = observer(() => { )} {issueDetailStore.peekMode === "full" && ( diff --git a/space/components/views/auth.tsx b/space/components/views/auth.tsx index cb36a6146..87ff8c366 100644 --- a/space/components/views/auth.tsx +++ b/space/components/views/auth.tsx @@ -1,3 +1,5 @@ +"use client"; + import { observer } from "mobx-react-lite"; import Image from "next/image"; // ui @@ -5,7 +7,7 @@ import { useTheme } from "next-themes"; import useSWR from "swr"; import { Spinner } from "@plane/ui"; // components -import { AuthRoot, UserLoggedIn } from "@/components/accounts"; +import { AuthRoot } from "@/components/accounts"; // hooks import { useUser } from "@/hooks/store"; // images @@ -17,12 +19,15 @@ export const AuthView = observer(() => { // hooks const { resolvedTheme } = useTheme(); // store - const { data: currentUser, fetchCurrentUser, isLoading } = useUser(); + const { fetchCurrentUser, isLoading } = useUser(); // fetching user information const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), { shouldRetryOnError: false, revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnReconnect: true, + errorRetryCount: 1, }); return ( @@ -33,30 +38,26 @@ export const AuthView = observer(() => {
) : ( <> - {currentUser ? ( - - ) : ( -
-
- Plane background pattern +
+
+ Plane background pattern +
+
+
+
+ Plane Logo + Plane +
-
-
-
- Plane Logo - Plane -
-
-
- -
+
+
- )} +
)} diff --git a/space/components/views/project-details.tsx b/space/components/views/project-details.tsx index ef51a4512..d9a9d9f59 100644 --- a/space/components/views/project-details.tsx +++ b/space/components/views/project-details.tsx @@ -1,7 +1,9 @@ -import { useEffect } from "react"; +"use client"; + +import { FC, useEffect } from "react"; import { observer } from "mobx-react-lite"; import Image from "next/image"; -import { useRouter } from "next/router"; +import useSWR from "swr"; // components import { IssueCalendarView } from "@/components/issues/board-views/calendar"; import { IssueGanttView } from "@/components/issues/board-views/gantt"; @@ -11,16 +13,30 @@ import { IssueSpreadsheetView } from "@/components/issues/board-views/spreadshee import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root"; import { IssuePeekOverview } from "@/components/issues/peek-overview"; // mobx store -import { useMobxStore, useUser } from "@/hooks/store"; -import { RootStore } from "@/store/root.store"; +import { useIssue, useUser, useProject, useIssueDetails } from "@/hooks/store"; // assets import SomethingWentWrongImage from "public/something-went-wrong.svg"; -export const ProjectDetailsView = observer(() => { - const router = useRouter(); - const { workspace_slug, project_slug, states, labels, priorities, peekId } = router.query; +type ProjectDetailsViewProps = { + workspaceSlug: string; + projectId: string; + params: any; +}; - const { issue: issueStore, project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore(); +export const ProjectDetailsView: FC = observer((props) => { + const { workspaceSlug, projectId, params } = props; + const { states, labels, priorities, peekId } = params; + // store hooks + const { fetchPublicIssues } = useIssue(); + const { activeLayout } = useProject(); + // fetching public issues + useSWR( + workspaceSlug && projectId ? "PROJECT_PUBLIC_ISSUES" : null, + workspaceSlug && projectId ? () => fetchPublicIssues(workspaceSlug, projectId, params) : null + ); + + const issueStore = useIssue(); + const issueDetailStore = useIssueDetails(); const { data: currentUser, fetchCurrentUser } = useUser(); useEffect(() => { @@ -30,25 +46,14 @@ export const ProjectDetailsView = observer(() => { }, [currentUser, fetchCurrentUser]); useEffect(() => { - if (workspace_slug && project_slug) { - const params = { - state: states || null, - labels: labels || null, - priority: priorities || null, - }; - issueStore.fetchPublicIssues(workspace_slug?.toString(), project_slug.toString(), params); - } - }, [workspace_slug, project_slug, issueStore, states, labels, priorities]); - - useEffect(() => { - if (peekId && workspace_slug && project_slug) { + if (peekId && workspaceSlug && projectId) { issueDetailStore.setPeekId(peekId.toString()); } - }, [peekId, issueDetailStore, project_slug, workspace_slug]); + }, [peekId, issueDetailStore, projectId, workspaceSlug]); return (
- {workspace_slug && } + {workspaceSlug && } {issueStore?.loader && !issueStore.issues ? (
Loading...
@@ -67,24 +72,24 @@ export const ProjectDetailsView = observer(() => {
) : ( - projectStore?.activeBoard && ( + activeLayout && (
{/* applied filters */} - {projectStore?.activeBoard === "list" && ( + {activeLayout === "list" && (
)} - {projectStore?.activeBoard === "kanban" && ( + {activeLayout === "kanban" && (
)} - {projectStore?.activeBoard === "calendar" && } - {projectStore?.activeBoard === "spreadsheet" && } - {projectStore?.activeBoard === "gantt" && } + {activeLayout === "calendar" && } + {activeLayout === "spreadsheet" && } + {activeLayout === "gantt" && }
) )} diff --git a/space/constants/issue.ts b/space/constants/issue.ts new file mode 100644 index 000000000..147d840fc --- /dev/null +++ b/space/constants/issue.ts @@ -0,0 +1,20 @@ +import { ILayoutDisplayFiltersOptions } from "@/types/issue-filters"; + +export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { + [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; +} = { + issues: { + list: { + filters: ["priority", "state", "labels"], + display_properties: null, + display_filters: null, + extra_options: null, + }, + kanban: { + filters: ["priority", "state", "labels"], + display_properties: null, + display_filters: null, + extra_options: null, + }, + }, +}; diff --git a/space/helpers/common.helper.ts b/space/helpers/common.helper.ts index 99e04e559..c6b305ef1 100644 --- a/space/helpers/common.helper.ts +++ b/space/helpers/common.helper.ts @@ -12,4 +12,9 @@ export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || ""; export const ASSET_PREFIX = SPACE_BASE_PATH; +export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL ?? ""; +export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH ?? ""; + +export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL ?? ""; + export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); diff --git a/space/hooks/store/index.ts b/space/hooks/store/index.ts index 3b7ef07c9..76b6f9315 100644 --- a/space/hooks/store/index.ts +++ b/space/hooks/store/index.ts @@ -1,4 +1,7 @@ -export * from "./user-mobx-provider"; - export * from "./use-instance"; -export * from "./user"; +export * from "./use-project"; +export * from "./use-issue"; +export * from "./use-user"; +export * from "./use-user-profile"; +export * from "./use-issue-details"; +export * from "./use-issue-filter"; diff --git a/space/hooks/store/use-instance.ts b/space/hooks/store/use-instance.ts index 92165e2bb..62aa0baae 100644 --- a/space/hooks/store/use-instance.ts +++ b/space/hooks/store/use-instance.ts @@ -1,10 +1,11 @@ import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/app-providers"; // store -import { StoreContext } from "@/lib/store-context"; import { IInstanceStore } from "@/store/instance.store"; export const useInstance = (): IInstanceStore => { const context = useContext(StoreContext); - if (context === undefined) throw new Error("useInstance must be used within StoreProvider"); + if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); return context.instance; }; diff --git a/space/hooks/store/use-issue-details.tsx b/space/hooks/store/use-issue-details.tsx new file mode 100644 index 000000000..56ee48627 --- /dev/null +++ b/space/hooks/store/use-issue-details.tsx @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/app-providers"; +// store +import { IIssueDetailStore } from "@/store/issue-detail.store"; + +export const useIssueDetails = (): IIssueDetailStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); + return context.issueDetail; +}; diff --git a/space/hooks/store/use-issue-filter.ts b/space/hooks/store/use-issue-filter.ts new file mode 100644 index 000000000..a80d9761b --- /dev/null +++ b/space/hooks/store/use-issue-filter.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/app-providers"; +// store +import { IIssueFilterStore } from "@/store/issue-filters.store"; + +export const useIssueFilter = (): IIssueFilterStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); + return context.issueFilter; +}; diff --git a/space/hooks/store/use-issue.ts b/space/hooks/store/use-issue.ts new file mode 100644 index 000000000..8ccd95ac4 --- /dev/null +++ b/space/hooks/store/use-issue.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/app-providers"; +// store +import { IIssueStore } from "@/store/issue.store"; + +export const useIssue = (): IIssueStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); + return context.issue; +}; diff --git a/space/hooks/store/use-project.ts b/space/hooks/store/use-project.ts new file mode 100644 index 000000000..0bc7d8f8a --- /dev/null +++ b/space/hooks/store/use-project.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/app-providers"; +// store +import { IProjectStore } from "@/store/project.store"; + +export const useProject = (): IProjectStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); + return context.project; +}; diff --git a/space/hooks/store/user/use-user-profile.ts b/space/hooks/store/use-user-profile.ts similarity index 62% rename from space/hooks/store/user/use-user-profile.ts rename to space/hooks/store/use-user-profile.ts index 5b1d8149d..042f16c0d 100644 --- a/space/hooks/store/user/use-user-profile.ts +++ b/space/hooks/store/use-user-profile.ts @@ -1,10 +1,11 @@ import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/app-providers"; // store -import { StoreContext } from "@/lib/store-context"; -import { IProfileStore } from "@/store/user/profile.store"; +import { IProfileStore } from "@/store/profile.store"; export const useUserProfile = (): IProfileStore => { const context = useContext(StoreContext); if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); - return context.user.userProfile; + return context.user.profile; }; diff --git a/space/hooks/store/user/use-user.ts b/space/hooks/store/use-user.ts similarity index 69% rename from space/hooks/store/user/use-user.ts rename to space/hooks/store/use-user.ts index e491d88a2..c935946f8 100644 --- a/space/hooks/store/user/use-user.ts +++ b/space/hooks/store/use-user.ts @@ -1,7 +1,8 @@ import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/app-providers"; // store -import { StoreContext } from "@/lib/store-context"; -import { IUserStore } from "@/store/user"; +import { IUserStore } from "@/store/user.store"; export const useUser = (): IUserStore => { const context = useContext(StoreContext); diff --git a/space/hooks/store/user-mobx-provider.ts b/space/hooks/store/user-mobx-provider.ts deleted file mode 100644 index 4fbc5591f..000000000 --- a/space/hooks/store/user-mobx-provider.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from "react"; -// store -import { StoreContext } from "@/lib/store-context"; -import { RootStore } from "@/store/root.store"; - -export const useMobxStore = (): RootStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useMobxStore must be used within StoreProvider"); - return context; -}; diff --git a/space/hooks/store/user/index.ts b/space/hooks/store/user/index.ts deleted file mode 100644 index 72660f100..000000000 --- a/space/hooks/store/user/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./use-user"; -export * from "./use-user-profile"; diff --git a/space/layouts/project-layout.tsx b/space/layouts/project-layout.tsx deleted file mode 100644 index 0411bcbcc..000000000 --- a/space/layouts/project-layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { observer } from "mobx-react-lite"; -import Image from "next/image"; -// components -import IssueNavbar from "@/components/issues/navbar"; -// logo -import planeLogo from "public/plane-logo.svg"; - -const ProjectLayout = ({ children }: { children: React.ReactNode }) => ( - -); - -export default observer(ProjectLayout); diff --git a/space/lib/app-providers.tsx b/space/lib/app-providers.tsx new file mode 100644 index 000000000..389d68ab2 --- /dev/null +++ b/space/lib/app-providers.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { ReactNode, createContext } from "react"; +import { ThemeProvider } from "next-themes"; +// store +import { RootStore } from "@/store/root.store"; + +let rootStore = new RootStore(); + +export const StoreContext = createContext(rootStore); + +function initializeStore(initialData = {}) { + const singletonRootStore = rootStore ?? new RootStore(); + // If your page has Next.js data fetching methods that use a Mobx store, it will + // get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details + if (initialData) { + singletonRootStore.hydrate(initialData); + } + // For SSG and SSR always create a new store + if (typeof window === "undefined") return singletonRootStore; + // Create the store once in the client + if (!rootStore) rootStore = singletonRootStore; + return singletonRootStore; +} + +export type AppProviderProps = { + children: ReactNode; + initialState: any; +}; + +export const AppProvider = ({ children, initialState = {} }: AppProviderProps) => { + const store = initializeStore(initialState); + return ( + + {children} + + ); +}; diff --git a/space/lib/index.ts b/space/lib/index.ts deleted file mode 100644 index a10356821..000000000 --- a/space/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const init = {}; diff --git a/space/lib/store-context.tsx b/space/lib/store-context.tsx deleted file mode 100644 index 1eff1ddde..000000000 --- a/space/lib/store-context.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { ReactElement, createContext } from "react"; -// mobx store -import { RootStore } from "@/store/root.store"; - -export let rootStore = new RootStore(); - -export const StoreContext = createContext(rootStore); - -const initializeStore = () => { - const singletonRootStore = rootStore ?? new RootStore(); - if (typeof window === "undefined") return singletonRootStore; - if (!rootStore) rootStore = singletonRootStore; - return singletonRootStore; -}; - -export const StoreProvider = ({ children }: { children: ReactElement }) => { - const store = initializeStore(); - return {children}; -}; diff --git a/space/lib/wrappers/auth-wrapper.tsx b/space/lib/wrappers/auth-wrapper.tsx index 3b49e80d5..ba1fae2e5 100644 --- a/space/lib/wrappers/auth-wrapper.tsx +++ b/space/lib/wrappers/auth-wrapper.tsx @@ -1,6 +1,6 @@ import { FC, ReactNode } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import useSWR from "swr"; import { Spinner } from "@plane/ui"; // helpers diff --git a/space/package.json b/space/package.json index a10d190d2..48fe001b7 100644 --- a/space/package.json +++ b/space/package.json @@ -22,6 +22,7 @@ "@plane/rich-text-editor": "*", "@plane/types": "*", "@plane/ui": "*", + "@plane/constants": "*", "@sentry/nextjs": "^7.108.0", "axios": "^1.3.4", "clsx": "^2.0.0", diff --git a/space/pages/404.tsx b/space/pages/404.tsx deleted file mode 100644 index 4591f71f8..000000000 --- a/space/pages/404.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// next imports -import { observer } from "mobx-react-lite"; -import Image from "next/image"; -// hooks -import { useInstance } from "@/hooks/store"; -// images -import notFoundImage from "public/404.svg"; - -const Custom404Error = observer(() => { - // hooks - const { instance } = useInstance(); - - const redirectionUrl = instance?.config?.app_base_url || "/"; - - return ( -
-
-
-
- 404- Page not found -
-
Oops! Something went wrong.
-
- Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is - temporarily unavailable. -
-
- - -
-
- ); -}); - -export default Custom404Error; diff --git a/space/pages/[workspace_slug]/[project_slug]/index.tsx b/space/pages/[workspace_slug]/[project_slug]/index.tsx deleted file mode 100644 index aaec7672e..000000000 --- a/space/pages/[workspace_slug]/[project_slug]/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Head from "next/head"; -import { useRouter } from "next/router"; -import useSWR from "swr"; -// components -import { ProjectDetailsView } from "@/components/views"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -// hooks -import { useMobxStore } from "@/hooks/store"; -// layouts -import ProjectLayout from "@/layouts/project-layout"; -// wrappers -import { AuthWrapper } from "@/lib/wrappers"; - -const WorkspaceProjectPage = (props: any) => { - const SITE_TITLE = props?.project_settings?.project_details?.name || "Plane | Deploy"; - - const router = useRouter(); - const { workspace_slug, project_slug, states, labels, priorities } = router.query; - - const { project: projectStore, issue: issueStore } = useMobxStore(); - - useSWR("REVALIDATE_ALL", () => { - if (workspace_slug && project_slug) { - projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString()); - const params = { - state: states || null, - labels: labels || null, - priority: priorities || null, - }; - issueStore.fetchPublicIssues(workspace_slug.toString(), project_slug.toString(), params); - } - }); - - return ( - - - - {SITE_TITLE} - - - - - ); -}; - -export default WorkspaceProjectPage; diff --git a/space/pages/[workspace_slug]/index.tsx b/space/pages/[workspace_slug]/index.tsx deleted file mode 100644 index 635f3fdf9..000000000 --- a/space/pages/[workspace_slug]/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const WorkspaceProjectPage = () => ( -
Plane Workspace Space
-); - -export default WorkspaceProjectPage; diff --git a/space/pages/_app.tsx b/space/pages/_app.tsx deleted file mode 100644 index 363b61510..000000000 --- a/space/pages/_app.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { AppProps } from "next/app"; -import Head from "next/head"; -import { ThemeProvider } from "next-themes"; -// styles -import "@/styles/globals.css"; -// contexts -import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo"; -import { ToastContextProvider } from "@/contexts/toast.context"; -// mobx store provider -import { StoreProvider } from "@/lib/store-context"; -// wrappers -import { InstanceWrapper } from "@/lib/wrappers"; - -const prefix = "/spaces/"; - -function MyApp({ Component, pageProps }: AppProps) { - return ( - <> - - {SITE_TITLE} - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -export default MyApp; diff --git a/space/pages/_document.tsx b/space/pages/_document.tsx deleted file mode 100644 index ae4455438..000000000 --- a/space/pages/_document.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Document, { Html, Head, Main, NextScript } from "next/document"; - -class MyDocument extends Document { - render() { - return ( - - - -
-
- - - - ); - } -} - -export default MyDocument; diff --git a/space/pages/accounts/forgot-password.tsx b/space/pages/accounts/forgot-password.tsx deleted file mode 100644 index 494eae9d3..000000000 --- a/space/pages/accounts/forgot-password.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { NextPage } from "next"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; -import { Controller, useForm } from "react-hook-form"; -// icons -import { CircleCheck } from "lucide-react"; -// ui -import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -import { cn } from "@/helpers/common.helper"; -import { checkEmailValidity } from "@/helpers/string.helper"; -// hooks -import useTimer from "@/hooks/use-timer"; -// wrappers -import { AuthWrapper } from "@/lib/wrappers"; -// services -import { AuthService } from "@/services/authentication.service"; -// images -import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg"; -import PlaneBackgroundPattern from "public/auth/background-pattern.svg"; -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; - -type TForgotPasswordFormValues = { - email: string; -}; - -const defaultValues: TForgotPasswordFormValues = { - email: "", -}; - -// services -const authService = new AuthService(); - -const ForgotPasswordPage: NextPage = () => { - // router - const router = useRouter(); - const { email } = router.query; - // hooks - const { resolvedTheme } = useTheme(); - // timer - const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0); - - // form info - const { - control, - formState: { errors, isSubmitting, isValid }, - handleSubmit, - } = useForm({ - defaultValues: { - ...defaultValues, - email: email?.toString() ?? "", - }, - }); - - const handleForgotPassword = async (formData: TForgotPasswordFormValues) => { - await authService - .sendResetPasswordLink({ - email: formData.email, - }) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Email sent", - message: - "Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.", - }); - setResendCodeTimer(30); - }) - .catch((err: any) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: err?.error ?? "Something went wrong. Please try again.", - }); - }); - }; - - return ( - -
-
- Plane background pattern -
-
-
-
- Plane Logo - Plane -
-
-
-
-
-
-

- Reset your password -

-

- Enter your user account{"'"}s verified email address and we will send you a password reset link. -

-
-
-
- - checkEmailValidity(value) || "Email is invalid", - }} - render={({ field: { value, onChange, ref } }) => ( - 0} - /> - )} - /> - {resendTimerCode > 0 && ( -

- - We sent the reset link to your email address -

- )} -
- - - Back to sign in - -
-
-
-
-
-
-
- ); -}; - -export default ForgotPasswordPage; diff --git a/space/pages/accounts/reset-password.tsx b/space/pages/accounts/reset-password.tsx deleted file mode 100644 index 773acb10e..000000000 --- a/space/pages/accounts/reset-password.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { NextPage } from "next"; -import Image from "next/image"; -import { useRouter } from "next/router"; -// icons -import { useTheme } from "next-themes"; -import { Eye, EyeOff } from "lucide-react"; -// ui -import { Button, Input } from "@plane/ui"; -// components -import { PasswordStrengthMeter } from "@/components/accounts"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -import { API_BASE_URL } from "@/helpers/common.helper"; -import { getPasswordStrength } from "@/helpers/password.helper"; -// wrappers -import { AuthWrapper } from "@/lib/wrappers"; -// services -import { AuthService } from "@/services/authentication.service"; -// images -import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg"; -import PlaneBackgroundPattern from "public/auth/background-pattern.svg"; -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; - -type TResetPasswordFormValues = { - email: string; - password: string; - confirm_password?: string; -}; - -const defaultValues: TResetPasswordFormValues = { - email: "", - password: "", -}; - -// services -const authService = new AuthService(); - -const ResetPasswordPage: NextPage = () => { - // router - const router = useRouter(); - const { uidb64, token, email } = router.query; - // states - const [showPassword, setShowPassword] = useState(false); - const [resetFormData, setResetFormData] = useState({ - ...defaultValues, - email: email ? email.toString() : "", - }); - const [csrfToken, setCsrfToken] = useState(undefined); - const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); - // hooks - const { resolvedTheme } = useTheme(); - - useEffect(() => { - if (email && !resetFormData.email) { - setResetFormData((prev) => ({ ...prev, email: email.toString() })); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [email]); - - const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) => - setResetFormData((prev) => ({ ...prev, [key]: value })); - - useEffect(() => { - if (csrfToken === undefined) - authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); - }, [csrfToken]); - - const isButtonDisabled = useMemo( - () => - !!resetFormData.password && - getPasswordStrength(resetFormData.password) >= 3 && - resetFormData.password === resetFormData.confirm_password - ? false - : true, - [resetFormData] - ); - - return ( - -
-
- Plane background pattern -
-
-
-
- Plane Logo - Plane -
-
-
-
-
-
-

- Set new password -

-

Secure your account with a strong password

-
-
- -
- -
- -
-
-
- -
- handleFormChange("password", e.target.value)} - //hasError={Boolean(errors.password)} - placeholder="Enter password" - className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" - minLength={8} - onFocus={() => setIsPasswordInputFocused(true)} - onBlur={() => setIsPasswordInputFocused(false)} - autoFocus - /> - {showPassword ? ( - setShowPassword(false)} - /> - ) : ( - setShowPassword(true)} - /> - )} -
- {isPasswordInputFocused && } -
- {getPasswordStrength(resetFormData.password) >= 3 && ( -
- -
- handleFormChange("confirm_password", e.target.value)} - placeholder="Confirm password" - className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" - /> - {showPassword ? ( - setShowPassword(false)} - /> - ) : ( - setShowPassword(true)} - /> - )} -
- {!!resetFormData.confirm_password && - resetFormData.password !== resetFormData.confirm_password && ( - Passwords don{"'"}t match - )} -
- )} - -
-
-
-
-
-
-
- ); -}; - -export default ResetPasswordPage; diff --git a/space/pages/index.tsx b/space/pages/index.tsx deleted file mode 100644 index 8bba85cca..000000000 --- a/space/pages/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { observer } from "mobx-react-lite"; -import { NextPage } from "next"; -// components -import { AuthView } from "@/components/views"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -// wrapper -import { AuthWrapper } from "@/lib/wrappers"; - -const Index: NextPage = observer(() => ( - - - -)); - -export default Index; diff --git a/space/pages/onboarding/index.tsx b/space/pages/onboarding/index.tsx deleted file mode 100644 index 98404f577..000000000 --- a/space/pages/onboarding/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from "react"; -import { observer } from "mobx-react-lite"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; -// ui -import { Avatar } from "@plane/ui"; -// components -import { OnBoardingForm } from "@/components/accounts/onboarding-form"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -import { ASSET_PREFIX } from "@/helpers/common.helper"; -// hooks -import { useUser, useUserProfile } from "@/hooks/store"; -// wrappers -import { AuthWrapper } from "@/lib/wrappers"; -// assets -import ProfileSetupDark from "public/onboarding/profile-setup-dark.svg"; -import ProfileSetup from "public/onboarding/profile-setup-light.svg"; - -const OnBoardingPage = observer(() => { - // router - const router = useRouter(); - const { next_path } = router.query; - - // hooks - const { resolvedTheme } = useTheme(); - - const { data: user } = useUser(); - const { data: currentUserProfile, updateUserProfile } = useUserProfile(); - - if (!user) { - router.push("/"); - return <>; - } - - // complete onboarding - const finishOnboarding = async () => { - if (!user) return; - - await updateUserProfile({ - onboarding_step: { - ...currentUserProfile?.onboarding_step, - profile_complete: true, - }, - }).catch(() => { - console.log("Failed to update onboarding status"); - }); - - if (next_path) router.push(next_path.toString()); - router.push("/"); - }; - - return ( - -
-
-
-
-
- Plane Logo -
-
-
-
-
- {user?.avatar && ( - - )} - - {user?.first_name ? `${user?.first_name} ${user?.last_name ?? ""}` : user?.email} - -
-
-
-
-
-
-

Welcome to Plane!

-

- Let’s setup your profile, tell us a bit about yourself. -

-
- -
-
-
-
-
- {user?.avatar && ( - - )} - - {user?.first_name ? `${user?.first_name} ${user?.last_name ?? ""}` : user?.email} - -
-
-
- Profile setup -
-
-
-
- ); -}); - -export default OnBoardingPage; diff --git a/space/pages/project-not-published/index.tsx b/space/pages/project-not-published/index.tsx deleted file mode 100644 index 0bd25dd6e..000000000 --- a/space/pages/project-not-published/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// next imports -import { observer } from "mobx-react-lite"; -import Image from "next/image"; -// helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -// hooks -import { useInstance } from "@/hooks/store"; -// wrappers -import { AuthWrapper } from "@/lib/wrappers"; -// images -import projectNotPublishedImage from "@/public/project-not-published.svg"; - -const CustomProjectNotPublishedError = observer(() => { - // hooks - const { instance } = useInstance(); - - const redirectionUrl = instance?.config?.app_base_url || "/"; - - return ( - -
-
-
-
- 404- Page not found -
-
- Oops! The page you{`'`}re looking for isn{`'`}t live at the moment. -
-
- If this is your project, login to your workspace to adjust its visibility settings and make it public. -
-
- - -
-
-
- ); -}); - -export default CustomProjectNotPublishedError; diff --git a/space/services/api.service.ts b/space/services/api.service.ts index b6d353ccc..a5fe3e93d 100644 --- a/space/services/api.service.ts +++ b/space/services/api.service.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import axios, { AxiosInstance } from "axios"; // store -import { rootStore } from "@/lib/store-context"; +// import { rootStore } from "@/lib/store-context"; abstract class APIService { protected baseURL: string; @@ -18,14 +18,14 @@ abstract class APIService { } private setupInterceptors() { - this.axiosInstance.interceptors.response.use( - (response) => response, - (error) => { - const store = rootStore; - if (error.response && error.response.status === 401 && store.user.data) store.user.reset(); - return Promise.reject(error); - } - ); + // this.axiosInstance.interceptors.response.use( + // (response) => response, + // (error) => { + // const store = rootStore; + // if (error.response && error.response.status === 401 && store.user.data) store.user.reset(); + // return Promise.reject(error); + // } + // ); } get(url: string, params = {}) { diff --git a/space/services/authentication.service.ts b/space/services/auth.service.ts similarity index 100% rename from space/services/authentication.service.ts rename to space/services/auth.service.ts diff --git a/space/services/issue.service.ts b/space/services/issue.service.ts index b6f2e3be2..aa54e500e 100644 --- a/space/services/issue.service.ts +++ b/space/services/issue.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "@/helpers/common.helper"; // services import APIService from "@/services/api.service"; -import { API_BASE_URL } from "@/helpers/common.helper"; class IssueService extends APIService { constructor() { diff --git a/space/services/project.service.ts b/space/services/project.service.ts index 2e173d282..bff754595 100644 --- a/space/services/project.service.ts +++ b/space/services/project.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "@/helpers/common.helper"; // services import APIService from "@/services/api.service"; -import { API_BASE_URL } from "@/helpers/common.helper"; class ProjectService extends APIService { constructor() { diff --git a/space/store/instance.store.ts b/space/store/instance.store.ts index db4fd87a9..4a410d851 100644 --- a/space/store/instance.store.ts +++ b/space/store/instance.store.ts @@ -18,15 +18,18 @@ type TError = { export interface IInstanceStore { // issues isLoading: boolean; - instance: IInstance | undefined; + data: IInstance | NonNullable; + config: Record; error: TError | undefined; // action fetchInstanceInfo: () => Promise; + hydrate: (data: Record, config: Record) => void; } export class InstanceStore implements IInstanceStore { isLoading: boolean = true; - instance: IInstance | undefined = undefined; + data: IInstance | Record = {}; + config: Record = {}; error: TError | undefined = undefined; // services instanceService; @@ -35,15 +38,22 @@ export class InstanceStore implements IInstanceStore { makeObservable(this, { // observable isLoading: observable.ref, - instance: observable, + data: observable, + config: observable, error: observable, // actions fetchInstanceInfo: action, + hydrate: action, }); // services this.instanceService = new InstanceService(); } + hydrate = (data: Record, config: Record) => { + this.data = { ...this.data, ...data }; + this.config = { ...this.config, ...config }; + }; + /** * @description fetching instance information */ @@ -51,10 +61,11 @@ export class InstanceStore implements IInstanceStore { try { this.isLoading = true; this.error = undefined; - const instance = await this.instanceService.getInstanceInfo(); + const instanceDetails = await this.instanceService.getInstanceInfo(); runInAction(() => { this.isLoading = false; - this.instance = instance; + this.data = instanceDetails.instance; + this.config = instanceDetails.config; }); } catch (error) { runInAction(() => { diff --git a/space/store/issue_details.ts b/space/store/issue-detail.store.ts similarity index 99% rename from space/store/issue_details.ts rename to space/store/issue-detail.store.ts index 3bbf0e581..b6734640b 100644 --- a/space/store/issue_details.ts +++ b/space/store/issue-detail.store.ts @@ -55,7 +55,7 @@ export interface IIssueDetailStore { removeIssueVote: (workspaceId: string, projectId: string, issueId: string) => Promise; } -class IssueDetailStore implements IIssueDetailStore { +export class IssueDetailStore implements IIssueDetailStore { loader: boolean = false; error: any = null; peekId: string | null = null; @@ -431,5 +431,3 @@ class IssueDetailStore implements IIssueDetailStore { } }; } - -export default IssueDetailStore; diff --git a/space/store/issues/issue-filters.store.ts b/space/store/issue-filters.store.ts similarity index 55% rename from space/store/issues/issue-filters.store.ts rename to space/store/issue-filters.store.ts index e62e57933..d137753be 100644 --- a/space/store/issues/issue-filters.store.ts +++ b/space/store/issue-filters.store.ts @@ -1,15 +1,16 @@ import { action, makeObservable, observable, runInAction, computed } from "mobx"; -// types +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; +// store import { RootStore } from "@/store/root.store"; -import { IIssueFilterOptions, TIssueParams } from "./types"; -import { handleIssueQueryParamsByLayout } from "./helpers"; -import { IssueFilterBaseStore } from "./base-issue-filter.store"; +// types +import { TIssueBoardKeys, IIssueFilterOptions, TIssueParams } from "@/types/issue"; interface IFiltersOptions { filters: IIssueFilterOptions; } -export interface IIssuesFilterStore { +export interface IIssueFilterStore { // observables projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined; // computed @@ -21,15 +22,13 @@ export interface IIssuesFilterStore { updateFilters: (projectId: string, filters: IIssueFilterOptions) => Promise; } -export class IssuesFilterStore extends IssueFilterBaseStore implements IIssuesFilterStore { +export class IssueFilterStore implements IIssueFilterStore { // observables projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined = undefined; // root store rootStore; constructor(_rootStore: RootStore) { - super(_rootStore); - makeObservable(this, { // observables projectIssueFilters: observable.ref, @@ -43,35 +42,61 @@ export class IssuesFilterStore extends IssueFilterBaseStore implements IIssuesFi this.rootStore = _rootStore; } + // helper methods + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + // helpers issueDisplayFilters = (projectId: string) => { if (!projectId) return undefined; return this.projectIssueFilters?.[projectId] || undefined; }; - // actions + handleIssueQueryParamsByLayout = (layout: TIssueBoardKeys | undefined, viewType: "issues"): TIssueParams[] | null => { + const queryParams: TIssueParams[] = []; + if (!layout) return null; + + const layoutOptions = ISSUE_DISPLAY_FILTERS_BY_LAYOUT[viewType][layout]; + + // add filters query params + layoutOptions.filters.forEach((option: any) => { + queryParams.push(option); + }); + + return queryParams; + }; + + // actions updateFilters = async (projectId: string, filters: IIssueFilterOptions) => { try { - let _projectIssueFilters = { ...this.projectIssueFilters }; - if (!_projectIssueFilters) _projectIssueFilters = {}; - if (!_projectIssueFilters[projectId]) _projectIssueFilters[projectId] = { filters: {} }; + let issueFilters = { ...this.projectIssueFilters }; + if (!issueFilters) issueFilters = {}; + if (!issueFilters[projectId]) issueFilters[projectId] = { filters: {} }; - const _filters = { - filters: { ..._projectIssueFilters[projectId].filters }, + const newFilters = { + filters: { ...issueFilters[projectId].filters }, }; - _filters.filters = { ..._filters.filters, ...filters }; + newFilters.filters = { ...newFilters.filters, ...filters }; - _projectIssueFilters[projectId] = { - filters: _filters.filters, + issueFilters[projectId] = { + filters: newFilters.filters, }; runInAction(() => { - this.projectIssueFilters = _projectIssueFilters; + this.projectIssueFilters = issueFilters; }); - return _filters; + return newFilters; } catch (error) { throw error; } @@ -89,7 +114,7 @@ export class IssuesFilterStore extends IssueFilterBaseStore implements IIssuesFi get appliedFilters() { const userFilters = this.issueFilters; - const layout = this.rootStore.project?.activeBoard; + const layout = this.rootStore.project?.activeLayout; if (!userFilters || !layout) return undefined; let filteredRouteParams: any = { @@ -98,7 +123,7 @@ export class IssuesFilterStore extends IssueFilterBaseStore implements IIssuesFi labels: userFilters?.filters?.labels || undefined, }; - const filteredParams = handleIssueQueryParamsByLayout(layout, "issues"); + const filteredParams = this.handleIssueQueryParamsByLayout(layout, "issues"); if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); return filteredRouteParams; diff --git a/space/store/issue.ts b/space/store/issue.store.ts similarity index 82% rename from space/store/issue.ts rename to space/store/issue.store.ts index c6ed8ee71..bbaf47f79 100644 --- a/space/store/issue.ts +++ b/space/store/issue.store.ts @@ -1,11 +1,11 @@ -import { observable, action, computed, makeObservable, runInAction } from "mobx"; +import { observable, action, makeObservable, runInAction } from "mobx"; // services import IssueService from "@/services/issue.service"; +// types +import { IIssue, IIssueState, IIssueLabel } from "@/types/issue"; // store import { RootStore } from "./root.store"; -// types // import { IssueDetailType, TIssueBoardKeys } from "types/issue"; -import { IIssue, IIssueState, IIssueLabel } from "types/issue"; export interface IIssueStore { loader: boolean; @@ -26,7 +26,7 @@ export interface IIssueStore { getFilteredIssuesByState: (state: string) => IIssue[]; } -class IssueStore implements IIssueStore { +export class IssueStore implements IIssueStore { loader: boolean = false; error: any | null = null; @@ -75,13 +75,13 @@ class IssueStore implements IIssueStore { const response = await this.issueService.getPublicIssues(workspaceSlug, projectId, params); if (response) { - const _states: IIssueState[] = [...response?.states]; - const _labels: IIssueLabel[] = [...response?.labels]; - const _issues: IIssue[] = [...response?.issues]; + const states: IIssueState[] = [...response?.states]; + const labels: IIssueLabel[] = [...response?.labels]; + const issues: IIssue[] = [...response?.issues]; runInAction(() => { - this.states = _states; - this.labels = _labels; - this.issues = _issues; + this.states = states; + this.labels = labels; + this.issues = issues; this.loader = false; }); } @@ -99,5 +99,3 @@ class IssueStore implements IIssueStore { getFilteredIssuesByState = (state_id: string): IIssue[] | [] => this.issues?.filter((issue) => issue.state == state_id) || []; } - -export default IssueStore; diff --git a/space/store/issues/base-issue-filter.store.ts b/space/store/issues/base-issue-filter.store.ts deleted file mode 100644 index d3aaae5f7..000000000 --- a/space/store/issues/base-issue-filter.store.ts +++ /dev/null @@ -1,29 +0,0 @@ -// types -import { RootStore } from "@/store/root.store"; - -export interface IIssueFilterBaseStore { - // helper methods - computedFilter(filters: any, filteredParams: any): any; -} - -export class IssueFilterBaseStore implements IIssueFilterBaseStore { - // root store - rootStore; - - constructor(_rootStore: RootStore) { - // root store - this.rootStore = _rootStore; - } - - // helper methods - computedFilter = (filters: any, filteredParams: any) => { - const computedFilters: any = {}; - Object.keys(filters).map((key) => { - if (filters[key] != undefined && filteredParams.includes(key)) - computedFilters[key] = - typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); - }); - - return computedFilters; - }; -} diff --git a/space/store/issues/helpers.ts b/space/store/issues/helpers.ts deleted file mode 100644 index a862ca6e0..000000000 --- a/space/store/issues/helpers.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { TIssueBoardKeys } from "types/issue"; -import { IIssueFilterOptions, TIssueParams } from "./types"; - -export const isNil = (value: any) => { - if (value === undefined || value === null) return true; - - return false; -}; - -export interface ILayoutDisplayFiltersOptions { - filters: (keyof IIssueFilterOptions)[]; - display_properties: boolean | null; - display_filters: null; - extra_options: null; -} - -export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { - [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; -} = { - issues: { - list: { - filters: ["priority", "state", "labels"], - display_properties: null, - display_filters: null, - extra_options: null, - }, - kanban: { - filters: ["priority", "state", "labels"], - display_properties: null, - display_filters: null, - extra_options: null, - }, - }, -}; - -export const handleIssueQueryParamsByLayout = ( - layout: TIssueBoardKeys | undefined, - viewType: "issues" -): TIssueParams[] | null => { - const queryParams: TIssueParams[] = []; - - if (!layout) return null; - - const layoutOptions = ISSUE_DISPLAY_FILTERS_BY_LAYOUT[viewType][layout]; - - // add filters query params - layoutOptions.filters.forEach((option) => { - queryParams.push(option); - }); - - return queryParams; -}; diff --git a/space/store/issues/types.ts b/space/store/issues/types.ts deleted file mode 100644 index d1de0a5ea..000000000 --- a/space/store/issues/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { IIssue } from "types/issue"; - -export type TIssueGroupByOptions = "state" | "priority" | "labels" | null; - -export type TIssueParams = "priority" | "state" | "labels"; - -export interface IIssueFilterOptions { - state?: string[] | null; - labels?: string[] | null; - priority?: string[] | null; -} - -// issues -export interface IGroupedIssues { - [group_id: string]: string[]; -} - -export interface ISubGroupedIssues { - [sub_grouped_id: string]: { - [group_id: string]: string[]; - }; -} - -export type TUnGroupedIssues = string[]; - -export interface IIssueResponse { - [issue_id: string]: IIssue; -} - -export type TLoader = "init-loader" | "mutation" | undefined; - -export interface ViewFlags { - enableQuickAdd: boolean; - enableIssueCreation: boolean; - enableInlineEditing: boolean; -} diff --git a/space/store/user/profile.store.ts b/space/store/profile.store.ts similarity index 100% rename from space/store/user/profile.store.ts rename to space/store/profile.store.ts diff --git a/space/store/project.ts b/space/store/project.store.ts similarity index 57% rename from space/store/project.ts rename to space/store/project.store.ts index dd2c37c00..e382b6792 100644 --- a/space/store/project.ts +++ b/space/store/project.store.ts @@ -11,11 +11,15 @@ export interface IProjectStore { error: any | null; workspace: IWorkspace | null; project: IProject | null; - deploySettings: IProjectSettings | null; - viewOptions: any; - activeBoard: TIssueBoardKeys | null; + settings: IProjectSettings | null; + activeLayout: TIssueBoardKeys; + layoutOptions: Record; + canReact: boolean; + canComment: boolean; + canVote: boolean; fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise; - setActiveBoard: (value: TIssueBoardKeys) => void; + setActiveLayout: (value: TIssueBoardKeys) => void; + hydrate: (projectSettings: any) => void; } export class ProjectStore implements IProjectStore { @@ -24,9 +28,18 @@ export class ProjectStore implements IProjectStore { // data workspace: IWorkspace | null = null; project: IProject | null = null; - deploySettings: IProjectSettings | null = null; - viewOptions: any = null; - activeBoard: TIssueBoardKeys | null = null; + settings: IProjectSettings | null = null; + activeLayout: TIssueBoardKeys = "list"; + layoutOptions: Record = { + list: true, + kanban: true, + calendar: false, + gantt: false, + spreadsheet: false, + }; + canReact: boolean = false; + canComment: boolean = false; + canVote: boolean = false; // root store rootStore; // service @@ -38,14 +51,18 @@ export class ProjectStore implements IProjectStore { loader: observable, error: observable.ref, // observable - workspace: observable.ref, - project: observable.ref, - deploySettings: observable.ref, - viewOptions: observable.ref, - activeBoard: observable.ref, + workspace: observable, + project: observable, + settings: observable, + layoutOptions: observable, + activeLayout: observable.ref, + canReact: observable.ref, + canComment: observable.ref, + canVote: observable.ref, // actions fetchProjectSettings: action, - setActiveBoard: action, + setActiveLayout: action, + hydrate: action, // computed }); @@ -53,6 +70,20 @@ export class ProjectStore implements IProjectStore { this.projectService = new ProjectService(); } + hydrate = (projectSettings: any) => { + const { workspace_detail, project_details, views, votes, comments, reactions } = projectSettings; + this.workspace = workspace_detail; + this.project = project_details; + this.layoutOptions = views; + this.canComment = comments; + this.canVote = votes; + this.canReact = reactions; + }; + + setActiveLayout = (boardValue: TIssueBoardKeys) => { + this.activeLayout = boardValue; + }; + fetchProjectSettings = async (workspace_slug: string, project_slug: string) => { try { this.loader = true; @@ -68,8 +99,8 @@ export class ProjectStore implements IProjectStore { runInAction(() => { this.project = currentProject; this.workspace = currentWorkspace; - this.viewOptions = currentViewOptions; - this.deploySettings = currentDeploySettings; + this.layoutOptions = currentViewOptions; + this.settings = currentDeploySettings; this.loader = false; }); } @@ -80,8 +111,4 @@ export class ProjectStore implements IProjectStore { return error; } }; - - setActiveBoard = (boardValue: TIssueBoardKeys) => { - this.activeBoard = boardValue; - }; } diff --git a/space/store/root.store.ts b/space/store/root.store.ts index 77fce9613..8b6b10f51 100644 --- a/space/store/root.store.ts +++ b/space/store/root.store.ts @@ -1,13 +1,11 @@ -// mobx lite import { enableStaticRendering } from "mobx-react-lite"; // store imports import { IInstanceStore, InstanceStore } from "@/store/instance.store"; -import { IProjectStore, ProjectStore } from "@/store/project"; -import { IUserStore, UserStore } from "@/store/user"; - -import IssueStore, { IIssueStore } from "./issue"; -import IssueDetailStore, { IIssueDetailStore } from "./issue_details"; -import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store"; +import { IssueDetailStore, IIssueDetailStore } from "@/store/issue-detail.store"; +import { IssueStore, IIssueStore } from "@/store/issue.store"; +import { IProjectStore, ProjectStore } from "@/store/project.store"; +import { IUserStore, UserStore } from "@/store/user.store"; +import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store"; import { IMentionsStore, MentionsStore } from "./mentions.store"; enableStaticRendering(typeof window === "undefined"); @@ -16,33 +14,36 @@ export class RootStore { instance: IInstanceStore; user: IUserStore; project: IProjectStore; - issue: IIssueStore; - issueDetails: IIssueDetailStore; - mentionsStore: IMentionsStore; - issuesFilter: IIssuesFilterStore; + issueDetail: IIssueDetailStore; + mentionStore: IMentionsStore; + issueFilter: IIssueFilterStore; constructor() { this.instance = new InstanceStore(this); this.user = new UserStore(this); - this.project = new ProjectStore(this); this.issue = new IssueStore(this); - this.issueDetails = new IssueDetailStore(this); - this.mentionsStore = new MentionsStore(this); - this.issuesFilter = new IssuesFilterStore(this); + this.issueDetail = new IssueDetailStore(this); + this.mentionStore = new MentionsStore(this); + this.issueFilter = new IssueFilterStore(this); } - resetOnSignOut = () => { - localStorage.setItem("theme", "system"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hydrate = (data: any) => { + if (!data) return; + this.instance.hydrate(data?.instance || {}, data?.config || {}); + this.user.hydrate(data?.user || {}); + }; + reset = () => { + localStorage.setItem("theme", "system"); this.instance = new InstanceStore(this); this.user = new UserStore(this); this.project = new ProjectStore(this); - this.issue = new IssueStore(this); - this.issueDetails = new IssueDetailStore(this); - this.mentionsStore = new MentionsStore(this); - this.issuesFilter = new IssuesFilterStore(this); + this.issueDetail = new IssueDetailStore(this); + this.mentionStore = new MentionsStore(this); + this.issueFilter = new IssueFilterStore(this); }; } diff --git a/space/store/user/index.ts b/space/store/user.store.ts similarity index 90% rename from space/store/user/index.ts rename to space/store/user.store.ts index e5bfc41c6..2f228b629 100644 --- a/space/store/user/index.ts +++ b/space/store/user.store.ts @@ -3,11 +3,11 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx" // types import { IUser } from "@plane/types"; // services -import { AuthService } from "@/services/authentication.service"; +import { AuthService } from "@/services/auth.service"; import { UserService } from "@/services/user.service"; // stores import { RootStore } from "@/store/root.store"; -import { ProfileStore, IProfileStore } from "@/store/user/profile.store"; +import { ProfileStore, IProfileStore } from "@/store/profile.store"; import { ActorDetail } from "@/types/issue"; type TUserErrorStatus = { @@ -22,12 +22,13 @@ export interface IUserStore { error: TUserErrorStatus | undefined; data: IUser | undefined; // store observables - userProfile: IProfileStore; + profile: IProfileStore; // computed currentActor: ActorDetail; // actions fetchCurrentUser: () => Promise; updateCurrentUser: (data: Partial) => Promise; + hydrate: (data: IUser) => void; reset: () => void; signOut: () => Promise; } @@ -39,14 +40,14 @@ export class UserStore implements IUserStore { error: TUserErrorStatus | undefined = undefined; data: IUser | undefined = undefined; // store observables - userProfile: IProfileStore; + profile: IProfileStore; // service userService: UserService; authService: AuthService; constructor(private store: RootStore) { // stores - this.userProfile = new ProfileStore(store); + this.profile = new ProfileStore(store); // service this.userService = new UserService(); this.authService = new AuthService(); @@ -58,7 +59,7 @@ export class UserStore implements IUserStore { error: observable, // model observables data: observable, - userProfile: observable, + profile: observable, // computed currentActor: computed, // actions @@ -94,7 +95,7 @@ export class UserStore implements IUserStore { }); const user = await this.userService.currentUser(); if (user && user?.id) { - await this.userProfile.fetchUserProfile(); + await this.profile.fetchUserProfile(); runInAction(() => { this.data = user; this.isLoading = false; @@ -153,6 +154,10 @@ export class UserStore implements IUserStore { } }; + hydrate = (data: IUser): void => { + this.data = { ...this.data, ...data }; + }; + /** * @description resets the user store * @returns {void} @@ -163,7 +168,7 @@ export class UserStore implements IUserStore { this.isLoading = false; this.error = undefined; this.data = undefined; - this.userProfile = new ProfileStore(this.store); + this.profile = new ProfileStore(this.store); }); }; diff --git a/space/tsconfig.json b/space/tsconfig.json index 9d3e164be..f7833dff1 100644 --- a/space/tsconfig.json +++ b/space/tsconfig.json @@ -1,12 +1,22 @@ { "extends": "tsconfig/nextjs.json", - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts"], + "plugins": [ + { + "name": "next" + } + ], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts", ".next/types/**/*.ts"], "exclude": ["node_modules"], "compilerOptions": { "baseUrl": ".", "jsx": "preserve", "paths": { "@/*": ["*"] - } + }, + "plugins": [ + { + "name": "next" + } + ] } } diff --git a/space/types/issue-filters.d.ts b/space/types/issue-filters.d.ts new file mode 100644 index 000000000..0ec82f40e --- /dev/null +++ b/space/types/issue-filters.d.ts @@ -0,0 +1,6 @@ +export interface ILayoutDisplayFiltersOptions { + filters: (keyof IIssueFilterOptions)[]; + display_properties: boolean | null; + display_filters: null; + extra_options: null; +} diff --git a/space/types/issue.d.ts b/space/types/issue.d.ts index 4b76c75e8..2b7d3e673 100644 --- a/space/types/issue.d.ts +++ b/space/types/issue.d.ts @@ -170,3 +170,38 @@ export interface IssueDetailType { votes: any[]; }; } + +export type TIssueGroupByOptions = "state" | "priority" | "labels" | null; + +export type TIssueParams = "priority" | "state" | "labels"; + +export interface IIssueFilterOptions { + state?: string[] | null; + labels?: string[] | null; + priority?: string[] | null; +} + +// issues +export interface IGroupedIssues { + [group_id: string]: string[]; +} + +export interface ISubGroupedIssues { + [sub_grouped_id: string]: { + [group_id: string]: string[]; + }; +} + +export type TUnGroupedIssues = string[]; + +export interface IIssueResponse { + [issue_id: string]: IIssue; +} + +export type TLoader = "init-loader" | "mutation" | undefined; + +export interface ViewFlags { + enableQuickAdd: boolean; + enableIssueCreation: boolean; + enableInlineEditing: boolean; +} diff --git a/web/package.json b/web/package.json index 06efa5a84..c3de0c238 100644 --- a/web/package.json +++ b/web/package.json @@ -30,6 +30,7 @@ "@plane/rich-text-editor": "*", "@plane/types": "*", "@plane/ui": "*", + "@plane/constants": "*", "@popperjs/core": "^2.11.8", "@sentry/nextjs": "^7.108.0", "axios": "^1.1.3", diff --git a/yarn.lock b/yarn.lock index 053bb68d1..6f2a4aa34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6917,7 +6917,7 @@ postcss@8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.23, postcss@^8.4.38: +postcss@^8.4.23, postcss@^8.4.29, postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -7945,8 +7945,16 @@ streamx@^2.15.0, streamx@^2.16.1: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: - name string-width-cjs +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8026,7 +8034,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==