From 9eac78ae83178c2facf1a5aaeb3b67f3dac5d1cb Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Sun, 12 May 2024 19:15:10 +0530 Subject: [PATCH 1/2] feat: changemod space --- packages/eslint-config-custom/index.js | 2 +- .../[project_identifier]/layout.tsx | 30 +++ .../[project_identifier]/page.tsx} | 3 - space/app/error.tsx | 5 + space/app/layout.tsx | 49 +++++ space/app/page.tsx | 20 ++ space/components/instance/not-ready-view.tsx | 51 ++--- space/components/issues/navbar/index.tsx | 2 + space/components/views/auth.tsx | 6 +- space/helpers/common.helper.ts | 5 + space/hooks/store/index.ts | 2 - space/hooks/store/use-instance.ts | 2 +- space/hooks/store/user-mobx-provider.ts | 10 - space/hooks/store/user/use-user-profile.ts | 4 +- space/hooks/store/user/use-user.ts | 5 +- space/lib/app-providers.tsx | 38 ++++ space/lib/index.ts | 1 - space/lib/store-context.tsx | 19 -- space/lib/wrappers/auth-wrapper.tsx | 2 +- space/package.json | 4 +- space/pages/404.tsx | 42 ---- space/pages/[workspace_slug]/index.tsx | 5 - space/pages/_app.tsx | 47 ---- space/pages/_document.tsx | 18 -- space/pages/accounts/forgot-password.tsx | 166 -------------- space/pages/accounts/reset-password.tsx | 205 ------------------ space/pages/index.tsx | 16 -- space/pages/onboarding/index.tsx | 131 ----------- space/pages/project-not-published/index.tsx | 49 ----- space/public/instance/plane-takeoff.png | Bin 0 -> 47818 bytes space/services/api.service.ts | 18 +- space/store/instance.store.ts | 21 +- space/store/root.store.ts | 66 +++--- space/store/user.store.ts | 183 ++++++++++++++++ space/tsconfig.json | 14 +- 35 files changed, 442 insertions(+), 799 deletions(-) create mode 100644 space/app/[workspace_slug]/[project_identifier]/layout.tsx rename space/{pages/[workspace_slug]/[project_slug]/index.tsx => app/[workspace_slug]/[project_identifier]/page.tsx} (95%) create mode 100644 space/app/error.tsx create mode 100644 space/app/layout.tsx create mode 100644 space/app/page.tsx delete mode 100644 space/hooks/store/user-mobx-provider.ts create mode 100644 space/lib/app-providers.tsx delete mode 100644 space/lib/index.ts delete mode 100644 space/lib/store-context.tsx delete mode 100644 space/pages/404.tsx delete mode 100644 space/pages/[workspace_slug]/index.tsx delete mode 100644 space/pages/_app.tsx delete mode 100644 space/pages/_document.tsx delete mode 100644 space/pages/accounts/forgot-password.tsx delete mode 100644 space/pages/accounts/reset-password.tsx delete mode 100644 space/pages/index.tsx delete mode 100644 space/pages/onboarding/index.tsx delete mode 100644 space/pages/project-not-published/index.tsx create mode 100644 space/public/instance/plane-takeoff.png create mode 100644 space/store/user.store.ts 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_identifier]/layout.tsx b/space/app/[workspace_slug]/[project_identifier]/layout.tsx new file mode 100644 index 000000000..d831fd234 --- /dev/null +++ b/space/app/[workspace_slug]/[project_identifier]/layout.tsx @@ -0,0 +1,30 @@ +import { observer } from "mobx-react-lite"; +import Image from "next/image"; +// components +import IssueNavbar from "@/components/issues/navbar"; +// assets +import planeLogo from "public/plane-logo.svg"; + +const ProjectLayout = ({ children }: { children: React.ReactNode }) => ( +
+
+ +
+
{children}
+ +
+ Plane logo +
+
+ Powered by Plane Deploy +
+
+
+); + +export default observer(ProjectLayout); diff --git a/space/pages/[workspace_slug]/[project_slug]/index.tsx b/space/app/[workspace_slug]/[project_identifier]/page.tsx similarity index 95% rename from space/pages/[workspace_slug]/[project_slug]/index.tsx rename to space/app/[workspace_slug]/[project_identifier]/page.tsx index aaec7672e..44ecd0aab 100644 --- a/space/pages/[workspace_slug]/[project_slug]/index.tsx +++ b/space/app/[workspace_slug]/[project_identifier]/page.tsx @@ -35,9 +35,6 @@ const WorkspaceProjectPage = (props: any) => { return ( - - {SITE_TITLE} - 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..ddf10a287 --- /dev/null +++ b/space/app/page.tsx @@ -0,0 +1,20 @@ +"use client"; +// components +import { AuthView } from "@/components/views"; +// helpers +import { EPageTypes } from "@/helpers/authentication.helper"; +import { useInstance } from "@/hooks/store"; +// wrapper +import { AuthWrapper } from "@/lib/wrappers"; + +export default function HomePage() { + const { data } = useInstance(); + + console.log("data", data); + + return ( + + + + ); +} diff --git a/space/components/instance/not-ready-view.tsx b/space/components/instance/not-ready-view.tsx index a28fcf3e7..5b94d92ed 100644 --- a/space/components/instance/not-ready-view.tsx +++ b/space/components/instance/not-ready-view.tsx @@ -1,40 +1,33 @@ +"use client"; + import { FC } from "react"; import Image from "next/image"; -import { useTheme } from "next-themes"; -// icons -import { UserCog2 } from "lucide-react"; // ui -import { getButtonStyling } from "@plane/ui"; +import { Button } from "@plane/ui"; +// helper +import { ADMIN_BASE_URL, ADMIN_BASE_PATH } from "@/helpers/common.helper"; // images -import instanceNotReady from "public/instance/plane-instance-not-ready.webp"; -import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; -import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; +import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png"; export const InstanceNotReady: FC = () => { - const { resolvedTheme } = useTheme(); - - const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo; + const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH); return ( -
-
-
-
-
- Plane logo -
-
- Instance not ready -
-
-

Your Plane instance isn{"'"}t ready yet

-

Ask your Instance Admin to complete set-up first.

- - - Get started - -
-
+
+
+
+

Welcome aboard Plane!

+ Plane Logo +

+ Get started by setting up your instance and workspace +

+
+
diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index de8633100..b63f86a44 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; diff --git a/space/components/views/auth.tsx b/space/components/views/auth.tsx index cb36a6146..29dfa5680 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 @@ -17,7 +19,7 @@ export const AuthView = observer(() => { // hooks const { resolvedTheme } = useTheme(); // store - const { data: currentUser, fetchCurrentUser, isLoading } = useUser(); + const { fetchCurrentUser, isLoading, isAuthenticated } = useUser(); // fetching user information const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), { @@ -33,7 +35,7 @@ export const AuthView = observer(() => {
) : ( <> - {currentUser ? ( + {isAuthenticated ? ( ) : (
diff --git a/space/helpers/common.helper.ts b/space/helpers/common.helper.ts index 085b34dc2..39f664ba4 100644 --- a/space/helpers/common.helper.ts +++ b/space/helpers/common.helper.ts @@ -3,4 +3,9 @@ import { twMerge } from "tailwind-merge"; export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; +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..9a48d10f2 100644 --- a/space/hooks/store/index.ts +++ b/space/hooks/store/index.ts @@ -1,4 +1,2 @@ -export * from "./user-mobx-provider"; - export * from "./use-instance"; export * from "./user"; diff --git a/space/hooks/store/use-instance.ts b/space/hooks/store/use-instance.ts index 92165e2bb..15fdaf84f 100644 --- a/space/hooks/store/use-instance.ts +++ b/space/hooks/store/use-instance.ts @@ -1,6 +1,6 @@ import { useContext } from "react"; // store -import { StoreContext } from "@/lib/store-context"; +import { StoreContext } from "@/lib/app-providers"; import { IInstanceStore } from "@/store/instance.store"; export const useInstance = (): IInstanceStore => { 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/use-user-profile.ts b/space/hooks/store/user/use-user-profile.ts index d7f4c5569..61bfe64f8 100644 --- a/space/hooks/store/user/use-user-profile.ts +++ b/space/hooks/store/user/use-user-profile.ts @@ -1,10 +1,10 @@ import { useContext } from "react"; // store -import { StoreContext } from "@/lib/store-context"; +import { StoreContext } from "@/lib/app-providers"; import { IProfileStore } from "@/store/user/profile.store"; export const useUserProfile = (): IProfileStore => { const context = useContext(StoreContext); if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); - return context.profile; + return context.user.profile; }; diff --git a/space/hooks/store/user/use-user.ts b/space/hooks/store/user/use-user.ts index e491d88a2..c935946f8 100644 --- a/space/hooks/store/user/use-user.ts +++ b/space/hooks/store/user/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/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 1dad8f337..d976a5b80 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 d27a23109..a10d190d2 100644 --- a/space/package.json +++ b/space/package.json @@ -4,9 +4,9 @@ "private": true, "scripts": { "dev": "turbo run develop", - "develop": "next dev -p 4000", + "develop": "next dev -p 3002", "build": "next build", - "start": "next start -p 4000", + "start": "next start", "lint": "next lint", "export": "next export" }, 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]/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 8318f0346..000000000 --- a/space/pages/onboarding/index.tsx +++ /dev/null @@ -1,131 +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"; -// 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.svg"; - -const imagePrefix = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || ""; - -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.replace(next_path.toString()); - router.replace("/"); - }; - - 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/public/instance/plane-takeoff.png b/space/public/instance/plane-takeoff.png new file mode 100644 index 0000000000000000000000000000000000000000..417ff82999890f25a8d61da4e174375b58f457a3 GIT binary patch literal 47818 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

;B*sK>V_Ek<3UIhOnk z)<|1~vv88A1|KZ(mFYD~F%_gAZrvEQ`W_IPm5l5$m*~C2)*(?*a;jiq5w#f5O=eJb z3!CR!zpZ2ryA2MUS*?0YWG6*2gyk_ythPG0Eg=+aU!b~X@CifnWcUj&&x2c8~7MY9P+V@aC2Qnc?TfG3$Q^QDWZ|z+1 z+9lLv=rC>ofvC=MItE0Fce3eDOHlnhgmU&+Z--v6p!n`{(W6%saf}2tsZ{9HpQ7kH zw(?oV<@ri_QoH>tuEfvQAF@2w=D zB6R+hgDANXbDTAYNR~wt2D$oA$GDraxe1k-&VEiQXbOu*b=Y1aa@QmGWI`e5&UoS` zJm>F*opB}U!V>8r@GNQ6MtSjCH%Acfhsv&&EFzt!MkCD=?H`gHmarqTJTB|0RkAb) z8L?W%M^)E*YADmWt^AX55;5`#Gb-!b@(iJ7Ag`ODC}C&l=gOA-5{}QcA#h4Y7MY-~;hXFneHE9_sZBB5R+srf zbNODoFB;hmeZ)~)hmxrVqy24T_}r%|O|-}Q+52CU$!BJ!y=Z^XiP&L%I&w2%>vs|J zUg#V(yd8nv-uVexqJ5nrw$g(KLkwZ)!gj@E8Vgo^`F4gIeSZx?CQrt6?y0~uDY@X* zG{`eDFG*)xOyj!rzrKn$cw*c$7Y+I8PvZ@_1UghiW7p^p<4||f zhMH7x&M^1?0Rhp+3J=1X^CVWLi;r3RV`pjR?WF@*D5)s<)Zi@0j&RU^s$ugmRK&8{ z{LM?Rm^E}Bk|-*=c<)Y`(+CTY;Sfb`vk8fp~-R0(c#yZ8=PdCWEu4LICaKOgnHdm>^E)YhQ z$~*p5fj0$GPIep*$KGzLU;le$?qfU@9!yT9YkAJIH@0*?O^rBqi13Z)K!4%LDO%+4DQB(wu@zx{d@<)ARtmU;Ko@Xz7Ox1AifJ-K$^{dpoY$ZJx04fI8I(*Ut-1hj1%6%tfE{ zBF1~7NVxa1zeM$u4RDD0?m;Po24_x8n;pj=w)`2rmHe1tIDNU3xm>RO^Ov*lAL)$| zG!e*gd-AU5SH54|ioMLVC+^p|Ltg1-z@^*r(B7r38UhJV0aR8I6X^^BRI@ZoPeqLI zJhPNb&>3!A@p&^sxYCT#Z46UClnOp|c(-K$D{h=W`x}$cWylali;`nzq}UWXofIam zE|P!F4*e;aL6^^)Lh%$Wm?XSo?t8O}h%o`Y4oKVE7~~TqOgX_>JU6qBUP}+3fu7Y` zPtZlP*!Dao?AVZM%A=HT8wyj=;2{{1`x}WJcvX4({&>=83`@2^CRtG0=s`1)gM9sy z^j^LHg%GBn#Cs~W7xmN6+0Z6H1{23pT(n}QdIjvgV2x1(RXXHwHBmoE9b3!7=+jcQ zan(9=K0pbygojnj$YL{7vdCdBOM^|CUm6zcUaWP(Xzs_lGHVxFB}kPjP&B6d94Qlr zCjv>Pig+|=Sc>H1?wDhkm74gJxYdj;bh9}Ny3uRvB$rNUFlon+Z7QD_4?Vw#4qU-* z6mWWZIXmn)fcA&D<;*^|nf zN5j0(PQj>vX$vWPjliMgM)~bSHbd_*#Vc<`jA8ubj>Pj$wB zGi~r9kv%n(}tkt?_4#S2U#80oH}^(;6|JzQ>+?YQc1amdmEq$*cmciqjU1 zq$0JgV}*RGVV02_xY4b_31nlGF+@48Tx`jOIc6NLi{32P9+S5$3+L(d#OmyX`*jqX zzV-mlvdVV~8`T`4G1eE03-4#X&2|4Xkxcb*4?^e2xlC7m9`GqbEO%RR8@r9~B+^4% zVKHipm21ko9@TkUL6~7(n_MSOI9+QEr091UO?0&Pe4 zxZmfLRvg=C^;_%Yd17P$?5x)1%Axba@FY0G_D{7%K2iKzOvWFnwV<~*x~z#Mu~L2Q zL*yZfQ_Mo9)Thb`ZlwxzZ7hdM&_%X;oKjvug&zB!uyX4q_XhSSG@;kxqR`O=IL}JF zNr>_p7;WbhfxG~rO4f>#)ao+n+od#hMw}5#lLj+OleaqC?DOW3S-<4duXI3({Kw7w z4L52?Q?efi7rlFjp`+>j*r!IJI)!f&e#Y-rjR7td8Udy?hCf&t55lX`>oi&oQ^|l4 zc+zAxmGaAv;xdJHQ~|Yh(=FS#+{mhdKwRL6xbiyX*Cz24Wm)7#CT#1Tab z)6D2Kb~2>Cr7_GHfpEUHCIyU7chu&3`nXKoxtS;pbnN1+hG9s@1|HsC7aH>XQUDg? z+_6S)oAB>7l@+@rRY3;1J*Xs~Ha)@C4LhwFtBgxonsD9bk*rwL*5WQLAHuv+|s;nSWqCmC5nRcy8V{ZEf-_eZFbc43?Y)k{#j z-JJU3rq{Je>dX}v4ffZUFNLWU^Pr((Bx#i;&X6BS5lFu1R1-Si_rvG8`puK}5|eI_ zudV%#|Nia2d|#(2O71Km(}XD+?eYrkeXSGgL_9PLC2r`n;-`_CHDl0|h2q4{Fhf;n zi$QfHChierT$t_)GdNl6W2q|ev>+}7Af!l5yeg?)<&l3_Sxtml)9U&1gLb1~B+?@N zHFNvd(<2{*AACO1_L(b&M8;4n-`LImzD#rO=?YviW5@FP>WnUOnjJqq7eQOZvv2`% z{$l)EYkVDYD1yE#blH!U zDKsjN{Dt)qC_;&OKP|<-QLQ=l&EE1;zBV&c+bh!eZh>>G+gS`dnqa~7;gxg+tH&tk zD&Up+nN{FoPqS!L;Mc!oJ#)4}x%amp!_Zt-Tf=4IxvvodM%kgjS0XUwr^#dM_(hK8 z;{rw%V+Ngpi{yXQq@HVcP6ORpwYWpzxRV@ryBrtFtC~D59^#))Rmhu`HVaOp*H-*# z^$u-uvdG2fzaxuX&$cofiI|VX8~Y8v>{B3QT027r0+FW#d#3IG#nceUG|`-p{(_GZ zb2_BL5|0Z9$$rw9b{zhQ%zKFT4$>L(`9Gs9^dxqrZ%FE>QkMCjrPNI|ruCBH0XgQ* zUoNLf*nCWj6e!2$Xmrn$VAV1>I(;l>>gR_AYvpSYtbw6v)uC%#{FJjp9^Q(rQhKDJ)uE*$2Ahh6P$+MV3USuXj{4vNa> zo8}tg>EW{fZf^Jyirz0N8>8C){P5~ykAQ|t7ETejMrE%NXHegfc4Sma5q87PrJmJ} zAXQJc|L_>9CZaB-PL9Pv?JGAgqtTbj9^!#~;Di;r#cjzAa+}@h@A}55q7=>=QAs1Aj>Je2j#6=CZ-7!(H6WE^7$Xtb*38P+ixCrOL&# z%ygiYG|ha)Cp!2L72op`X@wAXp_q*1^AvmZjFWsJV?cz*I{RDiQR;ir`}_Olo&XV8 z)Q|cvU71g^ywG8d(*?zRT%&yP(0N4FES_ap*;2y86$4r-mlX7MEUyPX$y1}u=Q|f? z+o+4LZV2+FD+1R;6<$`i`~nu^@w&?99hFYOUw)s)FEd?obN=r@;opbJ7Ub~gNxxA3 zbwo4zh1}10H9_ffFI9{$^C zeSz><9d&_FN}@*tV@6Qy{z-0VjL&&;ZLQ%sZ!p!dAl-3S;rWkra$l$~hdSSg)-2r) z(ZmqQ$hr~#OJ1Oqm6OB&wsDNiUotB_U@*h=jUXmfAL|?d9xgGrR;Lhgzcm{#M%Q*? zA9yyCgCPNYhUmJYkNX99CCU%Ee`on)L=ofhg)Jk(f1hYM1C`3aBe%MCquJQ7ll9uTtDuvfK@#6cNdPSkB?#+@>R$Kk@G1h_n z^a#2ag$M19@H2pnEv~+Q{j!J5#|bGC(xT7jd^S)pDpioaB&CAk2csd9B!07EYQwa1@LeI0E=1!op6{ zSwqd=PX7X|TRxqM8t!1ITwS95l(6_7gNyWMn>FpyQ>*r+OTJoW=>+Sq#Y$$-j|;L9 zUcllDpX{7GJxrI9h5XJ#Ul`x_NAF$ed|^+-LhfcH&Ik&jGzbxV@OiC)2zEehP{TJz zr$z|=eZ-Rb@xe^B!H;A=1fF-QVQCEcD(ec%q0TrI;M<`F(nz>Df0ReIGR)E)PO@{= zV5vJvZ#IE*<{CZva>9Ie$*31)EA-WJ<;6ElsjJ}5WLw3}xqnNsOVa@-WIkV>fq{OW z6`SKIz!$?o_4dkw6eD3N4@$R{COwQuE!DV}m9Ud?|0?l-F8p`u{`J?Nw|subp^ws= z+|0%>7Ld*@5MIJ4MMIh{KOkUopf*Q>Kf~!Y&10XWMbYTMS^KnJT|i@*Kdzb+7Y(LJ zGhAg^=CcgTb~q??*GXl79>#h-HzSXEd^0OQ7$TPmmZQ*qv;S0(*YhwP&rZ<=37r?82s3!!8Kexw$>EDS%vgw~DXp4h9{$gQ;MU85T&;Im2jqu)=ss`>-RxB!-{8%8pt!MOK&%~am_b~Q4g3Nj(lN)N9Y)SH^RIhNwaW zwNAz#*OZMf7cq1J>bg=8H^oavuU6VI_O4;BwCc4&L< z5`Iv_`=yz8V+oDab~u(!(#L+b?Z5sc9*CKaqq^3bw{_iofZQg_0(zkyZ`;G!o&BDK zsVgR1f){dEL!@}{j9}QCU>l0dnZfm*nG+uKc?*h+OcfFaoQ%QNu*!)u4I+s;zd^Ro z*nZT9B})L^9X9@k{TXh9C5rp;JJ@*G)%6~fv3;F>HH3`~UupYB^C(>uqj;y)Q`Abp z``hu(vETXxi{bLi51x08HYML|N93+HzDBGgTGZd2E-%~cPaM9V`P|P({75m8xXlS$ zMkVCEq)c&Yx|NXLmmJ+#&o#<6(9KTT$Y|Bu8}xaQJf~%$9-L5$*{EETJ@<5Qv0Fj; z^cn{;H)z3hpU{gY?0__7>x6+>hxJO zv&Fc=?P4YhS$==-c7EpebKhI4Dhiy_WPP6A^1ea6an{-TN+Rruq?q9-NQE$FpT_J% z$_pAb-P4ueV!KP+<$;-3E`MryGdISoaTC%S6WUtpT=A6mPQCRa{kF&G{eXtn+XZTU z=@99E@_eA0fpWn!Y-Q_^wi#>02rh?8JyJ=tyF8zZGo++j`hp4-q)RGJQ|e3M<=nvZ zK{BCh`Wt!qYp=653fIfQaxXanK?i|wrN~9^%Tkru;jOgd5m%G4Ua;ysR4ZzbfW0$s zY|_jZVUw-ytY&SFWOt548(!Z}2! z`BIu`V=#1h(lA*X3*SDj>wBkOfjkbKe|bD)sR~;Dz11TrN_;qV-=7^+%fLEdvJ`o( zFd1K>*K8~Qz%!CUzj5X0)Btv6KKR2Q3kE)L(9!%g0<2v}7EtPlKEb*>omTiOy!(76 zw>Qn)x&27JZ{WOxScH#@%e zd!OhJtEdz5BftD&u95#a#ROzvdRFX?NyqOfHPJp-S2OW*N19WvNxR&%bzBNUUK!{{ z(=r_GUbI@VK3?tZKxf0Uj`}|UsX$i0Qn#1KEk$>q#IXZ+DOOjdnoT>QKq+;XX~%nL zZvr;kI+be=)w=4z07hE@RM7-1l&Z{ErG|v{q_0){e5u|?eC&f~rZ7}v&{;JLcl~^S zfB(CDwD9!6efOPqB4zYad}>a`r{GrL^jd7~uUxqJyK8pl{ULZrJaE@zi(4_0YP|`~ z@j7I23Tdj%ikbL@G5JHgck4K;d+>2M^vDBY(NQyD{#+#3DwVl$_?sKVUd)?f25p5xHRpFb6#7c})r-$0e#f+-ow9| zsMlxV{QtxEB;c`M;+9)Z9Ukl(5NVJX;&XE&wl>R;T>OKT@Fd|CtHvAt;`)V~HzmJK zGCDWQKt?xk8R3wdgt#_q2~h0fR)=bE(BTKd5l78}d2?$p&?ih!s>coH2T#ZbY%H#* zMWrvl{265RWasB^IPpYe=UX77OUZx#;Z~SN5GgV1)~!1z3`=KajK3aNEyISeCRHiR ziL(&RYg88RMNk}i2OAj>=p>CESJh*Qm)1N1Qc zcJ2>_V-WPUz=@m-o5{h2-YRYRFWmQlfv4lqf=#zE_;j z>Su81ocuo6Z>B)3`Ll=T`+0wV-`P0d3?BOh?!D`kXNEy|1scX~L+|`~J8SZt4AYWW zpMO6;6+5&SyX)F1od3JUWX)^-^tyL#+LC@o{Crf~!fb<5{OOsNhU!HQVJU>+xx=Vp z=fHx)hhX-s3iP2LHVk#Inp#4B2zx{;s-@E9uR4i5Y>vqP^yKg2=B?!Fo6d**5F;Za z^nwd6IHb`?&H>6#M?v~bF342+OF;!7nNebd$ydC|Hf~TbY4-F__GHpt*ty5P*YOG@ zRot}4?&H4CC3$dEu;~B@b1apeLdJ1IkC2!$d_Yqs zo@5G{SkF~(o?s-#l@XRw4gvNFBN9W6PT;%h;>*%R%QdOef_YaGqf4xMyIt=Pt=XSC z>${KVlB)8%KC&@@Ti2DKR?=#f(^!4)>E~i6{z`0nKC|f9AM6rW+_WUtQ}5&FVS90fyAFwUp&(YV{Ib)p z`QTWcd{QN!D>ITx^W)mYEBn9rBh`I;(G`|!xujtl1ki_O;<<;-fh-L zAPZ+?>O06%*YxfXGiYMMj$#SfOFYwyhBiWtuLIcub%KHl1Z#*=&I4!Jn1X2!@-3Q- zbMkcwRV`=O8MyxT3SJkLB@06eHfNSv)2~vi#{4VxDFqpvaJVAcq7RuATSn)*V=Pzv zc>MoEauqeE$H8Ntga}d!Sa1tI>zAM}bp&=1Jh5urc%&Pe!O6?c_`xT~>*)tXk6x1? z*iJ2XeuO0dQJP=}*#L=#M)dBhH5i^V3)=e{DAIJLj>ks23_*1+$XCa}Uf+R`{WD3v~(F+c_<_$WZL>W#7hkW&Z z#^g)7Tg#rubBP38^6WvJ=CvhO$JE+sx^+7Ztgyd6BxiO%_e12z%HqExchPZU6n*32%}b&nU^>!m1;s2 z+e-n16BaTkN9OmNr|T5w#pgoZ-$H8Fuj}=Rv*yj4w-G${DXd<7<}hn#x8O5>?XFU) z20kjdCkX$^weZ2yUi-blEo1Dj7!OqgCVuo*HCbi_s$UbwA;qyF5W8lL1U5dr2{y0W z3=iGa2M5fbDM@zzybAhaOSy`raM(MbxWj{RKX!8cb)pR3r+^J zjp=XACC9>k=Gc~~09;dSPbKXVnt6W*x#lFR?*ZgKVXIP^qij`8K--ARHvzMqk0NpN z0&%;YlQg05n|u4A5jP4-%Ozs-d0q8PxV`hvec@WjY5ymZWE=;u9b!8x-iu_H*og&Jb zL|GoZq;IVJEGf_68HX{cZaWpe+h3N^!H#&fod)b|%R(gTmOihqum4+koY&y?I(Y0e zSh(=JqYvEunoENAt{>58dJ-#vHTZkFYDc@Ba>Bp`zx(5Od2}0l3qyaDZ0v04l7jg0 zF=E?7(8e(J!tvWMG_Q?q8i(;M6Y%JR8(?r|KOB7c0dVlagE0GmGW6F1XqWoszG2*a zElGx7J6J=NkueyXNdEf9dzvqL(J|H4u$N)O`1qWx-h2(^LLCscDBn)2MiE}8Dx}zqGW?kO#xuEzNIPciUAumq! zbx5x~B&-B&IYlz?CPlNYFw+WOr|^E3g9K(FX;t{+Ibkzb*%uWpIOnc619;+zSa~Xl z6L931zUAF35Hoc2R5ZyOwQ2&C1mR^F)bZGxCaTa^YpGy8>MFiG>Q_uD&51O?-XxWi zAkaV_FVMuA$?b|1)E9f90q{z1LZ6bljIMoxJ;t=X>0_~VVf+pGoJ6>5PV#k$Pmg!=)A(|>{ zwFr)z^#t^zK2(>+GzqetV5M!D6*>n=0`R?6hayoEJ4{>g7tbx1*yvY#OL@l)H#w&u zpGirgp)dzWF#muMjvxL5KBGEU)D&_6u#vt>%_?O|1epe!Ts4Ur^RFS?z*$!7U0-ZY zvJ;IIuKK|^wCOMmVQ00khE^an3716>6WV=6g|two-UvtXu?MAa_&n$lSqwJ1xO1Ib zh{81ybLYFzE{@9w1_s^%9@8G4Sa$;d@16fU>+$vN|3PwnA(I136??PBTx1Qwq%%+^ zCt>5W%E4trQ|_z6;e$>1^7seg^Dq7yL+r!`S{n)7dYYOz**DWKD z!ES;e(}a;%1cba18~xKzk#^;o^6eD6SB*WhG}MAwE}KS>i0LgM42H+$B)G zqL8LjxJD=_QGlYF?k3WrdR2#0W z5)DJWoobQ|95Zp?C|LDlO4Ae`TC)Y#Js!jQCm3vPlp&@_vV${VaJULl6u>|wgXbK> z;n2BxN)M2ZG#_2l7$zazZjv>(OONHNeh(iS9PIxG@R-){#JZFC+fQ9PaOFg3!(cRci0ZQ<-EtJ-jl+I;NNOrfXJ0ikK}TUB@u2-wE%eliaEEVpdBIw=RIcpR?|UH z+Z3UuXv%%OIRl=k_rn;PU|a1BeUOSkS!U*@0agc;$79f?g3V`^PlX7$!SB6Njo>*4 zt${KfbNs~4eJ$LOoLKK$HvoTJJ7kNNmI^^n#fndE4lQ*GRT#xx+}~BO=01sMD#Kn9POlmo8u~GKOj~$jeG+*4 z+kZXhp~ps7Zkr&BWpI)$u%-hx%#NnVF8xx9dQCYxMXWsU(VHd>mrn|iSo^*Vub8+P zeOFafu@xAaH2`zw55pmc4#4bLWmK_2A#F!DV7K6)nAH`v|es#RG>qt6|6Ruo!}D&;WA zGVf5J=9rVccF97nDK9~YJ+ zrWFRNf^rF}1AQ=a?jX!R7^j2!VAfCsVi-G7qu2zD;J#b2Rag;`;480OO8x*l32YGP zh7FsSF_yg!uU6!mF3QZPF&!lZi;C1Cw+_PO1u`|Si;Z+Aw{{bzWoIxsnf4s!m;hbZ zVJ=6BOGO?=8M?&Gb()DG{7%B{H2-pUkaSJ@^>zhrxOc8J@k&+7a?)fDiQ0Rnqk@6) z?v6r-`ZUju_qJgr&1@gMsq6a#=WEKGDim`FKqseH}q;)T>Oh336X*ko#V( zFIJj}#kR0Zy;@Q(5MWV_=7b0Z6L7JpMUhC0KHgj@2aWDTav`roSe8{*aEEfU~M)iMmu8id*N1i|*hfpaQS#g;>*Vr7wYB^7?3 zX8eoMLD9EPKZx`kD|XYS@x`sU`DV`88}QN=VM9!esggNFV@U_VQGZ3VW&Xm_9dxPG z&gCB4T-8)0o}B1%^5i|#C=}K)UVNW9SKeU&YsRx32odIO0)edG7%Vy4!U?!-?dE>? z{R1=e!LoV&xp~LAi`Uxu`MHzIm48NRRV#(TfdEc9_)#d))Ct$J#z^(dIQLI=9vIxa zt_F9m>sQaBd1phWpukO{m)AU3EV2noK)R?I*8Y9a`x>YNogiE`&!kUUxy_|b6n=AM16l< zqGe9s&QJzFqHc4r3cKqJu&7daha_{DD&_*Qzi$a{vxFa1iFr>w^PlYZY5E!`X@6IcfJt zY|Y^_#ufoj16~M1Vnrp!bG6|TA@3wicRe`!n#%^>q0pRTbr5u}?3~I>yj!grvUgQr zt`@`OtU%DL@FRl{y9NM zVi8@>Fj_Cc$~)$`-y~Sday}bz?hh~ApZxP#6<9oqHV1Q=dUMF^433{W3fy{GiB*JZ zU#?4S<^(A^PQSTl5H@T>vVjbUqf&`WmRubtCr(YQz*HqMa6gY)uTp(dB$ODAm|usb zi&Lvu^Y4w>`$T0=yzF4AzqX?me5p%kU zmFC2%e}m>+O2s-PYlEs3v5KiwilAJsV5g^qN>&lQIKGHQXw)Bt#@LE~PvYOd3P1G% zTz#b{*y#aJtWOhvdBLwvzwd$8)y+0kxv}Xw>e4FDoGA-FG1upsD_DIbU5hv;-(1Dy z3OJ8m6u#KnU>~YjG!f4qgoEbQV0bV<6&pwuJF}X=y#6t%T5|I2oS=s)b;(sP6lT5l zXpw+6eh>)CQi6+Fyw+8U5yZb;Auo!5G zF#A3QrnluJZAh0wroLsFt1MEeQ&|$Q9C7sXr!dr)!rYlD%$^C7;PO_*d|#~iEa_@e z%r5I=n~sGcSIE^QSf6O1;|i#wT0^a5iv*jSgK5E^YD~W_!9rIslH-%@7{)G;=L;0M zqGBzTN>Hg(pjxd$R8o=0S(ZV&)xthU`ue%^=YQvcyI*r2686RTNr&+!Uv|XNKez-u z_FH&j-39pbzx>}f-M5xp5vM$o!zXu`ZOu*A@k^F8! z&^$_0G%}?cs@55bVCT=T!O{g34x8DKip44hN0}A^f_P4rE-~chOJ3wwt5hArH0uk? zd5ViQH$ykMnh|ao%ZXYlQAwg=ilD)2Fdmm-v|WYqRtWVb!-mWdDNce!MXsnOK(0wr zeFy5sm8&5fJHG|PbPGhZh1*iwz`N#f&WW8XvLN!fVx1^tf9J!4@G$yXZJENZC&azC z<{02GuXBI(J)xF@xqe`j5-p)>JqEA&&;zpkInp;tK1*^xrqW)-GN^Up-rxO51b01x z4L`|nN;P>hxxaWG#CtMr;}+ZcDlybbDfHJes8tvYRx>0JoXQ-~3M%iHByyT}C?$b` zdF~xzb)EtF>@lV-B*sdR>GUIQn%AFQs$=J2cMJDh6-kj?rT;VczuG8as!Yg~7NdC$ zc>*b#g(o9k(lMfrQm=~8BT24hsG{Yludh!MZjvPUY~a&D=)!{!M#s3ugLj`kf^WY7 zKi6o?ljUB$?w|0)x{L7Uzx?F|Yu3h>#+hu|ZS?lN0rr*w>5dfn^ zc;Op!Rd7Ctv}Rjejzo&aG5zq;=gooz^F!d)9OqQ-(G9*OP~J(FMRZ2wf}<({Tr%Pv zO0-Ctg>Br7u#zw5%2ZV^YGnPerPT+c%_?kbQm8jno~R^Io!%nyHbu@TXOKtyaz%tp zlNM-=Chn7nv_0qm1_xI*L0>t>EeU983|4J{AyfcU(o&Ko=BHL+T6 zC74>Xz?~Y!kas*(-zv9QF#Yw)XMuN{Yo%(%N%%5PIXZuK)bWeND(93*6SCPRh6H|M zO8{%102mw3R#K>;`W!^7My<+_NHh6f^L}ohG;TX|#ED$bAIQ$&Jm+}; zrfVWIt$sDRBB^G=S|EM>%$CCx*J~85oI2H{8SRFe@`##TX;6p~HcHq0MhbyUFgdXr zzid&tI(e}AJi@;$$rVk)H6&KCoMx%G=5HKz%;Gn7{)SfHeddA;(h)qyx%fqoc=fvf z!4vB)#JPX@yZ1i$Xyc#bRC%OKuU$n8YqKrzf~hQj&2>xb`n}9vidd=MM;V9}i1xP{ zau#tThaN~^$x&4}W$6JhcTT_TUFViKW$Noxj6F@0ra5(q6N=<&)hBc)--1GAT|}$e z%i{VxS5~RjRyH7uE<8S7gRwd)(l$0nai)nBYzaUyY?vtpSTt$pllctl6RD1eh_W6Y zF2Tam!!S4is9IyI-nb@FqbyaGBXQTMB~3;81e!60U)?za+GzmXy^rqq=aQ|nkNvLg zea!EFe#P2E%N0SEX$CKSCRhG*t~*>haETD66)R2wQ~A!~lL(TPWo$F;3Q*=?Nxp>T z&xCy@IgqaBQC}BVOXP!-G+qQA9TLG!RM2a`v_a`pMg zAAkH>m;?{r^O_5ZFb{LKCg$l`UcK%=@Wi@nLH_joUw`b;$I}nTY2M@*^P`CnHXVFJ zn5Mxprnye?w2hik3AQm-WFmrb4xE+2F^7k6;&H(YXuY$yxL!x%{ z97CwY+$PgSFy#tVqbQ0S)IwO;zY%8jCs2uEO+MyUbU_K@i?w|M`|Ox!jDae_2zfyfrXz%zV+PKQ zIasaZav8}r({sC?>FerH$&wGLRh23#SdpHbTatuAP?QL_Hi1hvkO`oWnnN{_7tF$t z6SaU$(ZS!F@Uiv}4nJNUti5#Evgh6oJAhEHoUj|jXUfv-tfQ7(cPn`8^YFwv1$g86 zzxlU^AItta)wv;gt}DxZrq*xdnEFiP2zf(m8bG4}R0>BX8^`&x0Tvw=z~Y4w%sFrf z1_oBa1ggl>RQBRZ$!5w#EYz$eTCHgFbfmoV3dVY4O~r;_ zJ5?uW5>?-=@(UAd*3N^bjEV(I;HI--=RE(tRQvj2_Pj%G4Qd0gd+AGG`Vj0S#JHeY z8%@JNF2LW&MMoa{!^^;9pN1cgbqes>S3c*$Z(jQc_SmNQ-OQ>N^`ob`HUZZc#hqrK z={2IkW13AwgA-!IIOhNkM;^jZoklP`R6%7b{5mzLRLZgf4%NXUcED=r)AyrdImuGxGjV#yku>o<2)-FR;LH1b&W1WnE9Rw$uQTAI`47vXkHOk+#6 zR?-1#TsOdYoS^{@1L&I^tq1w{*-7X;zXqOfuEBog6X*wg2Uee<9c5sYGkQ{*u+8^B^JKHc@mOt^FHux__W zp7x?|mHXO6>w;dkchih{s`mwRhutx9b)%@N28A%Mv;pSK;LulA9T>GAwR2+@R4Fyu zPp7_6^^GA3uGp+ZBU zL<2qEXos>SI^UFPb8mn?)>4G;6`v_ofl5-z224i=GyVD0b7#_QI2421z#&8cR#cpt zIP=kYm_ww72s!XxL~Gt>h?BX;N*$%D-|w1(ogg#l=oG5_Tj!>OL#oz#ly|uDP9*EH z+~)^ky0Z$>v>cYo&^I&_X3Uwlt#4rPi{+uxr(W@jv$w$0goql(4{)&$YTCEqiFFF` z$xr@%)~b8iH`hOrzSP#F6@MLKb$zC)u}~==1AQqRb!Y}hFT@6RFhUY7p+B$;)k-9j z(M8o4kwX^-CJ{b2iNT7}zKGN(}eLyrx>t*d8SA}lIAt^AoW#5u(3C_>JO-QU~@ zqZ+xZQnbG;ED<=MuL1M>$5DBTF+7iChC_GHO{GBrg^~$d1Os;c_9Zynh{O$ z8DUUCVvR(K43cvgm}p^>%POy4KZndwm0ESWq{@p{i2VE0CVdmhtNozdiSW&Hgn4ec z;6>AJPMo}Wo13Jh9UWEEF)$q}FtnQ6k@9Cqb%e}J1-YqClq_Xfu3#r@ z5c-E_^1i{LTT7Md7x`H88)v-bE#t5YA#z^vyT8Jd>%I$5tW%6ned<>Ozqup$)#Dq} z6Ld4Bez&%6t2Tu~iVa~R&3(rl4sggkf~Lt(nkUP$x=RF6NldEZOQ8Lz)Cs1@d)ul} zSw9}ZSywOWx~6Zx{$4n+KZb${*O=jeTVez-WNMU^ii8y@CZej*SW-nLT!MN#l7ndr zvKg)BO%tta-eb*qJSRLuma_U@NTIJr;Gpt4IAA7P0!k{HmJ)|t4Uy_mjf|Pmkaga{ zps;3r01vDSV4|MqyNX@0KZGR8p&yB}R^>25lBg>DAg(E})s575N%hrITlUgWxi*7p z@*%l70aRrrrCtM6rKKprXGr9qqIxZZ^tl>xC6wh6#XPgmO|B|4`+QhZWkvJ8iP+7* zL-qZd_lehF$=CJ0X4Ybn7v#{}>}M+~UDF9P#Gm`2&9mzC0PT97e*uvDxAX3m*_T)g zhABJ|V8>OMc`LPkB-8<@_76N-DpkHiqTs8izwV7U!xX}k>z>6eAK(j?{v%lfPZ6G2 zcLU!0-rMFpuwmjS8#brUV4Pb6A(xFAgZ^p)ix1CGp`xE|Knx2PNK%a?v4&cm>PjzR zU_6O2)?Ug>k({8$Y>_KSmH!$f2^op-_p`15CM=txcBPQZ~OQRBa4nlNHsgVDGK z+gcUaJQ2cpJ%e^Lg(S&j4yie(Ap9c^ieP?uqi&cJJV+B4n>df(6H3k!%^DWK0 zQ3cGm=Pd~lqfEH5$Pp*3gf>cq@+<=QK;|wl& zDg^6^bvNNXAO62X?tGyAv$3({$V{8IX3vb__(d5kI0z(pirT)SsDn_cB~7NXvP)=c zpz26R(qa15(4HdOb2q6cU%6!tT=~ZXVQyaxSD$gaW5Q)QnR4!!DG8)3gnHHw+u}ah zI8lbtaSp9!g2b1q^p}*`JW|f{u~|iLzkjF#OXhBa62#~eOi+}!@B|H(G|S@s4?zxOU<$aafTesi6cbTuly6S{U`rq}Td~OydTs^KUl_P~%}RyZZxQmH zxg{9lm_&hd+c{AK3FN+3f@XK=M@0oIzJqcp#_t9F$*9bQ-&GJRp~_@zgOY%E1f6B} zyUG<;P%92yn8dU56jdH_e^9;aFqb`{bH{!M@+TK@qDG ztoeAXvU3`!V7~wfRg!6?3Z*J4)V^91e|I;OqS7^_I=JGr(@q8TVbbt+2}QazFfDbziM0;%Q=aKW6$9m2x~Z2fpi zD%TTcZ?k^VoJ_UP6wR1h;~B8AF(_5(gjS_(QIsJ`WiF|#w`(ipfF4)WM3agX79^!g z2uID!pvJdBRW#zHsfd*$!3uw?X;`hGKrxWV5FnS90$I9}iFt}0oHl+IB-e5RLo-4c z7$h>VDF`y1Tdn-7#;0mYvE%rz0%bgpSO=qOS<(NRckWcQtvjGQIN!gG;FiAG&L$}* zOE)&bVOr>lIMOfO!Vj`DYRV9b*n}{$gj%XpL?QhKs@0W2Il87s%impo`stfsFM$bY z%F=A9&vTtx+%dvWL_^OlT^l&8K^<|?3I9O8yEDhxBk4B$oRcoZ*6dRL#*;?K)i>|= zCXV~>fB4v&Z@c$vz_y)q)=M*)RzgXr#*d_`5f>^|`A*F+ReEemXFz2IZ8GCIp>osP z*9{u8RgH8Y2(Flgie=^uE(hdhPB4fJqCq0q*q93sZ61Pc6QbsBjLNYoNi-Gd4yiKh zDN=QOa13%X7uO;huS{LnXd{p$3h37p1>0q`s;T;80%dG1V6AGjO?YN)EADp?-X~pf zfq-s+O?d}RS~e1x7ur>&gpBE!Tcv8v!n(0%PHIk?WpKz~15}C=kt9HknU_HxZJif; z25YLeS7FGNLxDqhkt_jR^=2wDh5*|xZ75m5d zk0SX=i$h}^;wJjSXGY50O*OG4+Z{cBlY?rX04LH{=bv^Ba4yJHR7a9-6h)>YqPJ8o@hB><3#0IVNf7<$ ziP7<&od4!O8-=|PM=bu{$OCu3dI7pwuDSc}SNQtEQwWm4H9h4ffdw>#Vc1FRc48Gu z6=xT#S7e0$>B%ReL+yO>-COs{hx+N_u6x(~#g?&&OE*5TwZDmk6-dHVy=g(55vtQD z4D@=HV<<`04qHWxAEA}baM!CtB%Sv z%gwwtRl+&GY52iuBdxiP$;M1HjAP9bRAfVsO)M&Mv~oireOfBOCN~Y{{xfr}A&p_U zGzyy(3PH zGFhb%tqCG9OoSN=b30C1+6GaeBw`0 zGyz{I2(;HWe3Y+U@^Nw{>?C%pDOiyzy#Y_VpkvD#{G~46pG?Ep;LtnneCTr%jpp08 zY}rQI?F3TvABsvcLJ5VY2OUtGv>#B?QR9?!gf59|Kib~T8F5L)XN1<=92Jx_Bu!+n_N_{CHfb)IGXD-# zns5NB)WQCiG{uL~a-bryr71WpR_)xou&^Vw&g9m75f zS^ki+TkxJ=dBic_zX&{bDefNO3-D($g3s2uo!d~YE?hja^Fd6zmsrm^=^AXE&V=4q zaw{~m<@SZ=Mvv>JGL$-!kMG)2PkIaV$AFFi{Dzo(h^8hE~yfSUCRan+ZNOVnPG;dc* zFdqeHf4Uj^YaGqNO^E0Os?j!n=0x5nYwEOBhZKnmf=|$|`}>C$Lo=;OGR<>Zo%~j- zMok`VPNs^qs8XJ(Dpp^OV(6>3V4&85{z_Y#2~E9MF^P!lKt^E;AGa_cs!om#Z~{lM z0$8$FBHqYk0N2dk`F-V4&}O?KQsbvoCH$CFp|bRnCQ)Io#*P`1Xp*#{C3IG)A4#+Z zw0|B{`b1htB*&R0x>}Ml4XcT#30fp#IYwx!UWXV>!f9(9czYCvE93BjlNrnybZoEO zF;#FxATMkb)E8?iR_{#A^1{+tpaBK|c0s7&26+ zYw_1}6E=XiQ5xQsw9-|tIs5E&u%F?9dtZGf;p7^8W|kkh_y;S&W0yntS!o8>;G?}j zue#*8kCPF264-6T%HMF}iP)9AWzroOoJCgLv{R1Zy%`TZ^w8X~iR^#JCnlDRk2fKX z6P@eFb)jyW=b!2v)SySnwOk1qUt~^@#-!`|+Qhnyb^F4K{y4{ss0sG2wR7Qt4TE|R zaCt`1?o+4C5i8W*wVTZT+$JxmRYZ*mJ7abq%!nU_fj$nksEwqG#2U2qSfC~qf@!lT z0TVnUeY+^lo){g3+aFyBAah!EAQQ{gs!^pviMp@F`y(`OR?x3JP_Cniw*}==l8+k- zRGX{=O`5`dh?5-CQ$=FSJmDa)Bcm8Xuo1?P`)#F{Qr|1%56y|xlxB3kFZ-$kR=U(q>k=Y>}*syy!wSLS=A}eiRR9CtQVzw!f`VLf#N7IMzH2& zY9p(OQy^cjH*>0s;uG_I7*_}l^08MbspIwy_}$!(XSqFN^q#O*zGD!g2cGltmv1G- zl4koY9=PY#m*Jf{W2`y8bm`KS`=twQZ@`@&;UhRZ14;Eso8EV`Dfrn7@Rxi8E-ol; zA(xP=SB=0vjx}r6%p2X-x_)e;d3?RzvZP8)&;`lsw2E7=+nZwAjN^A?+A)~>NLh#J z*EHTb-63;)tGwusGsLRO=5bJ|6lem)jS)$(QG926EMAFW>mc zutBU1)ZoBq6ZAzb=&K~CTF2GUY&5m`lbxzpnou?83bJ`l^Fqtv!X1O)NkgDiF2;vh&nx-wOM%!Z8!JvY^=>edX8BiS@ z#Cusq5*5Re1!%SvK0_7sCL_iJ6?H4U+juUbNtGvxP#cXHCa{wyrdGWHaeWMu<`^&} zRat*}b_9zLtHa#ky88UK_=PDr5B9#%u1;V#v0C#l&C`>NRlqFgeo>l7?y92s$nInt zzVDBCo=5NtxEDJ+Yw8KRxmK%fc-hNd)|f5?IvGUKe%?ltFz{Q^BzzWl>~+|!#44c4 z^$J`(RbRn($v1D>=N}Fvq2QL=?)k+;z4hEiqb<$9OeX6S+q(zQJ^1c-7kya9->8YT z({E~0Xxvd@@}dhS#inR3&TF|k3Kn0F)hqDJ`;Gwa5GXim*_t->dS&u7<@$`qCC>Zu zl$4-Uf;j^T%%G1Wv1U*Tu^|s)oK%jb8dU|XiP02xkcVaYJ(*~fVB@v{7{_^*p`t{^ zTM7l4CUSuCC`=(QxXu-6>bgay7y#GlI;zAz)#*E-s86H|0fu%!Y@)~T6pj@^s$feQ zx@PG7yE0iWHb9IyL!ikiRYTs%Np!YiG|eK(wOdG{A*7^=PIvT+mIt9cFo0&=sw`hA z)TkKNP7-NEHJKs-$;ff7PC*4FiNG)s`Afw4k*Gy_RK1?S_(U7Vx1mxUZ$PVwq&hK% z#5#)izYT}Xi{O~UQkXTcO;&eu$1Nq=M62eSIMo9T#HveDAjYndVQ8K=nh#7aDpQac zP3@1M6ZC$xF5HiwX)WHL`&mft8RVsnFL?E%%65czp+lkeH zzj4_J{yi5UT28M1-5S`(ar3Qre|mhP`JR?YRY@{UZeok=?<+W~l|s~wuUKcyg2uOL z3y?cGf`%v!xmvPi${%NavCI;5J{%W7u|ymLA(nn|FZwc*&=RKW^8`!Mid3zFahkni z0esN@VWmih2g-0zbPrTSt=g~!Q6PvgleKVl8(chwA$U#Z+*E(U&nU({6+Tl{(omlW zlp-~axw?)}E~_d?ni#FpRFT6BNj2k=j^sQOB+C_P$SO;kR5?*<*Z6crj;yH;WGw>L zXR6G((odWzzh)*)x{65KNOmbNLlO)CLlTXuLuk_N%Sp5vLaD3^gp>nGq{752{H8Ku zJWxd|s2&?YTvbT8B2}ga3m3lWdNYObaa5^DszRkU#wL(VC!ks11_5ixf+WWrmcYz` zruA=9GiIrYx|M1ZWQWDZ0yy=P3n?u|;p%mPEM`o%;{I##2Ccyhe~@x=Uls-TW?B1@ zzP|DG%a@#A(cuCQwExF2*$>@|;+8^<8N~R%T}kDJ#je6j6qdOX3VHv7+NunuC?v zHRfmC*^#E*RQk3Bp>mz_B22n*yRME&`=v=2sxzS^>dscnI2Qvcpc1VLA8DY;R7E9+ zp(+?sNWYTgr-pnBh#*t!j%q?B+T7gMggQ2!NJ6b6u{P^XXpWELG>ZGy;mCtCIA%ct zL$$V6kva-p6C`y8_a=_dv!F+GQe(}qj|F_Xy3UL^-) zm7EmvQ9v?Fwr{t#=MMS0#K;{pZ%*zaNa&VF4u_mN4XK&q=I^u7 zppN&AI~@n{o_wfI6-aaZfp^Mc@Sz-Vn@M5~MFzDg5eQCRqYh?LM>hmZt!ie5Y<1cV z$!+Y)9IR7*7+3yNRs0}7hfKMVgsW~>isYAMAtdP6#6FQES0>ZDL*(PKC0A2BS#+8* z;hz?2G>v7xXoAb)y^IPqIta9H0HVr}91SHZhmu62vZ?`XiBu@kNU2ewN~s}IO`tR| zN>Zy+s>n1lW?xXz(=B}Fnyn1R>w-`dH2=oZ>?;D4n&V?oB6T?W&;$uJgMn%)&r!?= z&Hetl@01!fiaZIMahFHANLeBL5lX}RvN&5);{0A1zHMUp@{xTa4|IQpuF7-)s@8>G zweCIe1D!ujtXQ!kKwsI3vZ@U>a%n0eTB%i6sv$Q~tJFr<%7$MDy~yZI-qfiTdV~}1 zRKd}skJwUx&Ri!-TED6(q+TFZ$kQVVg$T5Xb*Q`rLV7R~40TMw`U5)ZR@16pZ6#yA zn>@W!5eV(f2M#`t3TvA*4Xe+p?up=k(YP~R++N2nS#WdRTB#eTeX08FPEeT5Yn6;l zB=Im6M-1kV+$5}&T&3YKq6VH1^oeD3uP`@fp$wK{==&7pEX}_{-O6+q5tJ4~8%@W& zGy_;^F3Es7@P5s1q$T`;JSMS}V2TS;wYm@>Rpq5fb54vJtsp zBYWlGts+?HL1DZI!r(rz+F=k4%!W#TpBh$7kfqFhMYFdU;;*U-ObBzXAyJ`5 zL!&~iX*VZP6HRM^+(MyFaVk+|_LalN2_JL9peYFz$+WJ5lNy>(o8wBM4pv%l;*lvF zb(k8a&Jrl44ZI(Z;{CWEk9Hqk#GL`<_Y9W$9%u%^=H<(md-C+~*hTQfx(o3AYp?%r zZ0KIUZ9Ih0_7KFpiVd<*JSE^^87EW<*a!!@x{p#L+DDd&8m=ikv9Sq#^#@>9bripp zwjxqluwE2XFyyM$t6~RDI3^#|G5KmHvg1{PLtBPaf!WGA!e=<%sKM{nEOykjQl~1N zF;}g;X!o^Iu;SVRNMWc_hgr49VPK+% z%+1CoNJ>TS=E~JP)T%}>&yOX2uX+qvPI5oEzL#-~gxX5v)MzmnYfy;U0I=c3K!=Zn z%D^D1&`3q+3Lj}%*LW7MJ59ROG3!#$xtdCu8uPXBof<+F(YJuu3rd4mDFSQQs#suE)CY-#OXGhHEo6$YI2EW@|@T8C6jlLhd5Pxj( zXw55M`O0{@r>b~(><)NS@Gd|TW?ebXqStN==FqsWq#AsxR9YB1vjBZ2Yp7HruZ!r+`S+%L>Fo0ERx#3omqGCxKl*GXn z&xQW-HkjA91s+9ztJ>ex-_^lTPU$`6M^)D}SZY9jZ5)QG6VO*0Lxr7XT$JDQ_ocf* zx|dqIQ@WOB*#)Gf8z}`uy1To}Wf71Lk?v6G6r=@2LTQly<@a|#zpv*k=XIUAX6DSy z`MkZexxl&P5kzN`RdH0Afsi&5L>_SF<#rR|vQXo}t_PKI(!DmOWD9n z3=XxPAMrEMC703K>n8`B4Gx~K>`7OHJP7i3t{{A1^Be-tZmzV;I~df9Fd;{ zV^w+~;Q455DU$8{R=u}#ZQ8x9iTU3v{9h~7B8R78xS#Qr36jW!IP`5+*wA@4k@cC& z&v0I&Cd%LObTCVN9XFW!k@)?xCAzPw0sH-ew`xgj7=COsA=)lZK?I`*9gGjjqNPM9 zMvS=zpc1bT6l|hsG1Ba2`FsWyMg}Q;fl}X;zV6L-nPiT|wpiR7Ntu zvj7!Y#&IW}_fw>HEn|sPu}a65P+&1+R|}+Pt!~Xn$fm$X#N5Je zvo215vND8(hNdxKkr#v_5_4w-@51!oc3H{8r#i(*CODsu;PYhCH#zI&1_^7n_RGnb zoP^hsiFW7^2eKuUKAoRF;eZm&u3b6b=O|;~QQ&}z@mJe-Z1AKU$!b}|k@XC?#bY74 zM~n8*BQ=Z7+bw=D*^smu6Wzviv*$MraB6j{1o6ckaLud;+rSm5r1%4O~l5`Qce;0iJ zkWs&L2f;45($xk8d` z2tykZx>G)vJkioNVbKlkyzyL`l zwjyqkHf!RLL~rQyu$6_tJI?TR`>6P)AoC9+fS%;72L2aXAru2?u@6YW4Ydtvg=uA{ zfL%0kMD4JN=Y;)8B*Q}J-4nFJOLUvdxWK#1=@i-ei1mLeo-q5}G1|iwV zjxh^UdaX{pN)>ND-oVdgLcU(lj=Q0yaUm7*_}}b@19ls8)VcS*x;o_PkwK9Cam?88 ziqCor>R1{FM1(ngvJYoOJv)m+Mky6hTYg{H8xqvcJe^hMc}g9HaoV02Ht!o=enpDjhImywEEOTjU-vJU0xLZ z2+-TiP*CWa7WhSs7WldIB4o!na>&30Gswz-3lg938jAxpFuZHbP4|TY zH8@@lon^oIQNr=F1P0Z*2brs~V^RNh8v8t+J-rW!Gsgi}+A$+y=bCk8RCm$4@v*ijtnahM_owI5{{Sb&z&!!T2kRlShl|khBkFQW zmO>dc{0^tk9LFu@&h}1?Su2x5dm&LpW$!A*LYC@Qy}~39`fyuoR(xBc-bqJMbgg+u zQ7u;1NEKy73{%o1Rk}ihE1lyZ3%zC0ok@Q1Z$-i+`(wQiYXWte@1GD>(|8Md&%Bn; zHQ#k{)fZ%4=c*_WUnO7Gmz0W`#0+#+M?(+kFg&oSkv z{6NJaiQsOts*mCj#yJZ!=4z=HPb4S>`+{k-OO0oh=Uuf2M_I@S{uH{0LT@D|E3On6 zpGTj-?VxTZ34bWPRt0KEG!YC=AWp(^6?`X2dwl_<;3J)3>7mhDCnW*%;Y`R12*-+N zd}Z+G&JYc%SR~E+FHm6_uA=cYJPu?6Q_JD=e7oBBn4?qel1_VN;`Qk0AYI*5pZ8N8 z%8TuluUzK?K@`)1so@qAo^bB3e2f@mXd?=wgyZ=1!iOn0TEk#HCN1w+`}~{xBq`B5 ztgwpM%TiMKh|$YdPXDEa2@OrLz z&3+9j-#lhyP;}9U+HObX+QQNiMk&)k2;bB1$g8cCoNcVGf#Lq0&RT3B;Cc$ z7{z`;ctdPEcmT`MiDp>q6wu+AKP*}ygfMA$I~P}Yp}W=;Jvsicp`Z6GhV%(;w^c}) zb>zJb7nA~2H@ut~f#e2_Wl-~xgR{RxspmS05KR~`nvnZ(!TEVa7}AVWpqg21ZtmD?y5uvOkEt%tTAa zv#a0lU(@^}%Nroz%1ck+>7@Fr!IuKr!I%U+7JYs7Ze@*ElXRv_BMlr^5Bn5zny8%# zs@5~07>p|9OAQBOXR^7iTgC-T8@oKXussa5uy(taM2`f8J88auyqInVe|RJC^ZftMCE)Ai$G5HgwmfFbYoq4^r3u6Wgw`vYL6G0_iAJD&6Zx=trlIowY4L3 z4v~dDP}`A9kRq|wNkd&!OT|*P^23ae;`EqZ*w9GsHuZQP<~U@}?>7Lh$VgDMlgfKV zNelmnrHKZT$0xUs7M$D0XN0!k5~I{SzL$ItRY^h=p&Yp&Ly@bvf;oGj&X=ILved=? z{enALvia2whn)ZLw)7ibBQsAGyH~-xxt=v|1DfE$`fILyF5-t3^Um@Kdwuv6ZduhN z9+OtPx56R%F+$hTJCQ7NmMy%LweGcMyH}G5kC( z`r{v*3mdulEr~Rh0Zg7{25|_(Jn#@_#t>CTFsx>l>4U~RF|r_rN*b$m^Fk&ex~oZN z58l?hS&tZy&wP!^IwmJMcFD5vd2x=^Syn}g6GHxq__a!cEjb5L1IleEiO|+YSX)I^ z=^+x?Bf7r*;C9%^dv5(WhRLWrU5;)?SXgg^JimgnL-CqXcB@I;ka-NHi#WKJ_wu9*|cg>8N@)(<6uN^)gt$M z`s$OT=szgm)lz-z#R?!a9b4hg2T1WO{oo@7y%>dI65E;NS`SdYQw8Gl4`p;#9H{8t z`t6d$;C8u-y3}RnjrYx>+!eort`n>OKpe$`Csh$?)39|{79~q+K}gWD3SsI%`J}IMWx-0a%kTRL?@b20Lk}!Kz6jv&yoESA)8$z*MNCqsk|z^h&8bWA?@p+ z<)QH6Mr-|)e0R1R%+$oC%!=y8=od?hVYS=7>+y??Pdi?T3fu|=(YC?+$2&pzjB>}# zvz=i9d_8q2lN*aqqRTo9v zmS|zIIvIGhb!2n-#>qAeFX(?ZF%33mDYrBWygocqRaK?>$!zxKxCKpNO@cvgm=sis z!J}CmIv4;OZuFPYyh}rWXwJorA&uZ>Nv`aX5JszT=q%~sQ}x)&(CSUMA!DynOcmTF z9Vvz+qOt`Pj#ID=%W1USW^`0o+sdpk6(nq4-Z`o-Zhu_Z_LZT@D0`FIwV*WD_G^eg z=q#(Smh(^jqPr;!tP&iZ^|1ItuFT$b149LwUHF_ZS_{QXkpREc+1GjU6Z%{eYf-kn zW;Kjrv&Q+Ec&eT$LJy-kPb;0a8Iq=NkL&6m^b(UWiMu3%% z3wCP@ZcFl9P9#RCm-T&O1cnIl97mb(t8 z479p8ssX4zQABR_1k%g3Ynw2tC|wuhF*{*0zMQe}CMS2oQn&C*k*WY|5;`l2VhE5H z>dcedVMU%a*HSne5D0Q$i>jonS#o`>HjL7RuWWp@*L~FuaPTf=E@`bF1{cLXyEJ`2 zavV2bPh%ZMm?^v{(%eFys^p5$$M)VMPz0ECf|9~2br4gwLvhtMw&R8=IU>k!Z%xAD z>y6aZ-=-gmpU}PuI2(Gh@i^0kJ4uUxm-Y=AfZimY{?bL*A^0)3;Ngn>e@-u{Js^>zxT1+{W#b{T=q>EI-|T)Acuv6KUB_X zf7aEQaxG5(*9Z5N$)k$N7&k4o>rNGV^wQP@A_^Q)_cA*vr%Dn?{Yg#Z#NSP{FZa3i zn@5|Q%pW$0k5*+{ZyEEC|2%|&wv9ytwQiZ`LH$7k8aVch60}kFmk?2w$xjyzIO??J zM~%a+{FyTgaUMaxzx)E~-KdYg^o(mX&9d^5$#f;EuPfBYc9NT_?bZ(DMA@*0`f~Ur zmvq|2u8`OpSqZ2k4jP1E3`)J*_PJG$&tzC1-u2ua8Ta&czBVHU!~6@ zygf*mel>8mZ)EcRrjpg+&T;A!cJU|o-%L12k8fEmf1mol6!S$*`}v+pV;*n4L7M~r zBH@(6pky{#D-x70#LDsPI`Cs+VY49%w=G6WEhGN+1ccG-FVTtY80F9vkSOy3W|v5F zL80GzcH^`X5;Oa+F3kIggzu0Kf4Ty>DzWn=}K+-d6I%gBrdBL`dMn^S@~O( zxS>CZ8opo`+q&Xd&m#8N4@I?re{r0r1 z(gipt57E2+=I(}iVNby~vKRYU!MK7TAWqRe4OFH%NPKXIY z>|8+fja?m{R2&}x){rTaio|Zb*iCsG+c;%;d#b`8hz~d`nO6XECqN75?{Vi|F*@@N zr_DXA>UlqW!#=E6)eH5gyY8iz`E?Yf< zrZ<`jw;hIte-GJCWWE0T)hcX2dj1uc?A>+)WrV`PNh-)PCM`2>>+ds)>btxhl%4=H z9|$ru;S_!#RQg%Y4q6Hnc&jXgjao`oRCv|NIA`%Uve)N~G#*=s76Px_JfuIN0Q6f1 zgMS+OAJHwlWP=Pbf3mh;l-_zhqfBR2BQ;Dlg(iZomKG@4X(Vjx4-AT=Kl|I8Y= zYEoK-bE`W`g(JE2t)NG>f2F zNX(ovib1UoMX47jB#Z)sUY>BDl4Cp?P|87sfJ*gLI`KET<{VcD!+)-XGTr?7xZ%0Q z*jw9uvI!i~@&Ta0HrJ+$_^ApRhR!eB6J+9Vai z^BvJVxsQCLMiV+jg1$auNjwT`*%*u|Vw|8(Zkk`TsQSQSx`a zTxJE5mdysrUciiWHjx{r;WLJJ;iGQBp@GJGb*aA6eUMLaQV}pL5Kbge!3W>!=GLWv zX6&sOqvLGjCaizRNmlV`mZG1nKnJ^@WLW;~>ydiF4J3$uKY`}NVS{<#U#vh(t`@n3 zh-<>Y=fcX%vpaa7ybZL+`sLNGRzg6-Zjm5^fwo#m3)^p9xIC$XcRkZw3VT~7>gDof z)#W@Z>qKBgJ#cohzyY@9*;(S_uJ_3EyJw?u9%>NDA9-GHN{xcR(|>2>_e#9-!=0mD zy;XCaJ{p+LpHgrMGuslM?PRCS@#jA;JR5l3b2lpX-`a+Nwdwcn2ah>qq+Od++nlh- z6TZYu=aS`BZ9MGwICfEt#D$pcRtp(2%_%Bi6>xa!AV?sRI&!g&m$5{j?x=hI#hT=| z=zvj!fVv$ce)--is^TN2<>yRgM=xLW?$4P1=PMb*IToCf>VtfhEVp|%dzi@AwjZ=Lf-HkYJ$@*}0#KWv(Th)dqfw*N{mXwfa3P-1Nd<~Sk*q$5^O9KBV47ad zi_TK=s$#s7hfH9BT(tjr8pdc~ftZr)&NYAc5aiv-D0HR*S2QW>Dh%iY8j8oK#h3y3 zfDzk8Rw&oZfv$>iGxJIm;j(j7$cj%@CU(W8(^cHEr>)uU|2gaj;LW|mQI&WXZMGp3 zujYe};~QI}YEBLYrJB|5DYfS6+mn~#K2~!|a>l@(zWTbi-ddMIepE)NSc;j8*ru&V zqSppHCx5>AJ389`^uIlI0GeUNN(r&I#o{8C`JMqdLCYhaBXz5gy26S>Ql!9D7HZwF zJchwnlrfQMW#j__THC795SSXB$EmoBQ~!0xfyC{vrqIgu^62O}`F}6b04U2QpB2lw zeJY5)w;8RSNYTT2P30qGz@HMRch*acj6I!4fegQP#(XM{0s#(~RodNx$~=3OOx>+L zbirE>CjkrKf8IX_6IdMMmR!(6AT!3fr|S2ck{+Ax!b3v$-ZUKbIORx?`Bw${GH#t~$<%g1+8yD8kc;WJeo}T?@U(B%p z*?F=W!`e7iwY#T=cQ;MQN-KT!f#=M(6pA#idorf9=$rMFyi$Yj9Q=R6|E>Oejd*i* zMMQ9zupdE8-lcWrk;ME&A(v>Bo>Hw2EB)p~F0L}Efo8H0YbFv_om)VwNP*jeU#m7Q za^qT=Y>O30P7uf$2F3|qLBt8iV)6i2C)_~zM_xlrnSFZMpq9*vk zk6l^}@G|#ABZx79rU6xv@zo2no0>~ckr#}U@&IsEtNZfHPRrZMM;NhF39b}&qy`gX zqxOxCXl@nwN0kZD0uA7{8;U820SX*llkg-k!Yirw2kDO3sDQGAqGSSpz9num;I;?9b(<6M!7@42TPNOm){|t*qf1 zyX7+5`%EX1_Mhne?Mbagm0Dxz=DbSo-@(WS9l)L9S=%L2)1QW;k?~T4iDIYiIBe_p zcCqZmMbhoZBS78GshhKVgs*RO6#H$Tx5Y(YLQ`_)IP<|#jeKQVIv8oxT!(HxsPQMf zo@IGVLf~&-X|B>EA@Cl6D0;ieHkG@^Ahda|cnt5bH?lVKj2j8j5Ue84L4%o!K9Jr`S+7$U1 zNe1OAm&y#)kl@c`Juo0(u|p+%rTkvCWluKs?pMcE5!*x-uO{#p;w({{SXyFH-^rm zznR+_8hQI9TNd~%K9X28{8G8DKe}d`%kvm1y4(0}Bpm&FJ@r##tS4$lf}$uznsRKV zh&KheX~M9w6&~TC$vLNrroQ6$0l)SOJ_ebQx!0JJ4S<26J)N-p`u)LPUo+ z^g!eV6>P(pJ8tuQYMCJz zfDk+){ZY5?q7Fwa%9A-S6R6Jw8E97UbpTzUG^ZL8C6_Wn1;d_7(xGD6K%IwG+5SZv zBc(Z{MTeK2fPjF!j_l1&^Q;=9{6Qv~ zXzw| zBiwyFO0A(~P8PZdn{&UB%E}6mdkQSXkYi?t8+R){qyk3iBM?cC zpUX~IDn_eV+R9w5D===lLH*Ms_`F!w=g{rH7i$HK-n0nN`I%B-28Sy9z1L1`P*&$f z84qCvf?yw2_}LkzR>Go6Ea|dS#;)k`U5soJlOb8JEt1d4S~OIP{T)0%JhGoc^!<>e zd{JX~3{DQ+(v(C`6%1F-oG<$Xg(&$Qkd%ES>F2q;uoU^C(DN1BYb$kl&1I^KzGA%n ztTgPi0!5XuYfyxucA=miTPip+55(@qp2@y3?1$I5p!^YiiW_UAeurNrN#g;_pY`!WrwgO}v6#sbTgExwzMI`X=p{RH4`G_~xGy`?s2I}ye)2%Q zHuu~23Pq)2GACRmgCC)9UomEMmkm|RWrr|)C6}%aQsfo@d@KU%n&*Fe(`TQ1F2^ zcIR~1*Kz`%Eo=i3VsD4LhUK0|2e(xG*28gdUTW1!bx!lF(J@zaYk@)#+9K6<$G1vw z*|TM+P|&Hgqe|v zxK$2kgSkPmAr)f4XUBqdkyYvyxNOyHn2^BZ5gI+hH%R|_P2agK{0mEUw>B|?4L+)TU`l-BVME=g zp1@XKza&;Pc}cWePt%&OgY;r0CFd+MEew()F%G|%g;FunHjPuz?Q*k*7ut-UqZ8s5Y#5!~t?KfKUILME35v1%piY1xd5Kc|_ncfKSrO zd6|n00*%e(xML_B>f4Y?RyJnyk~_i5z8b}s7!s7i%7ir1@~0hA^i=fkRm(9THeGP$ z+DEAmGxw9DqZ21jh?2PmwZ*?lhCR)U$2iHczAmlXpp{_#Z2b66KdSCvKF_U>xv4@Z3Yy4zJOTn(!=nMzs{=H33(oCeW|HZyF~5nUhSQkLHusHDm+8h@LLK$&MS* z8K8_=!noeL8ZP>xSzE6qa-&~;7RiE>7lsdYsydA3kG3QuBehNVbOa-IPKz;{r z2Dk|4nU46CI!MQ&G?c|!O$77a(DrV{YQLDdUJJ`z?nzAOvc>}lZi0H%Md62M)+s9L z2?rED)e7dtedO)}IE%JvY2l=+Ctp4e{Tccp#^P3`@9|@c`1YYuJq{elB2^^#H!1={ zz1jpbVU!yq3Bys6wa6Nj& zm>Ae)F&%rQ=P=pCGI{A*obTc1vNlkCBQS0`ykt#*^)O|WkiPOK5FqozV(HZxuC2Kk z3U~qHPiUqm@5=wxeXaV(TjShSEdMZ2hv9wWo)w2Er1Hi{UIGTH#uc*D@_a^8M+F{X z%sPtj$dcaA#Gfj})fw!t-vSB>5a1v%+l$rRb>_kF=KVvE)4CgXy88^Nvq+XRET>^c zVj2V<6;>s6+wLNuvut|L8=Kig#Q`#mIvKJr?PiPU+sqZP+57&m3yey1!A-2;ZeLrdrw&Y8RLcCK`U)=1O?B-y(>?CsueUV=J%tLUSdk&d*I^S&vsHAM zSQ=eDJ@?8T=rvIeuwcPpony7O4V{zQ!%PQzAdkUfnsd&we@t%Bx)G#K-9aC2MjoWo zzCnt19FjPppY1&tSn%Q5be-k4`iLICUk~GU{592l0^1$m8&8w3HOySaxEXe&7K-bX zkQ9yX5OU8aRH$o;K&RxNO#!J@)8 z&X&4m#^VqRtuRCX>4h*vQ>aMn3;jEv>Fl$8@Bh%li?MmKSXXHLyORIYR5JZRgV7V5 zi}dGNQE>vP*uG5ktYEeTAs8^2hV-m=KHt$`CasIr{Oq&C`{VW|szZVtvXEyht-wSo zyT~xc&wi?opTLKyPAP~-M&`Rb4IiJ!cJrOW7>Yw0nx}Wjx)Xur$bKn?pPDDCx-fX% z66rmA6Lzu07YI9@?alSLy}U40JL0!dEX6&aOn?}1PAf0UVo=e58@f%}Wx?4&(k_M@ z9`=M^Hx`{U-^gC~iN~rd6|?R)EWS9VcX6m=Ty2+~nrS?RJjD~yGwu9}f1>IZ8dFl9 z!5mpf^QrlDUOZ{hr&$a1h+OfZSm{-1Z++=x>hj%DblKfSa&o+D1CF?*SWa8AHFTNEgKA!-6+?b%cRi6ZSUQ zn^)c5x(#0;`wL=Z#Fr#Zy645xJ@7ERD6${DcGk1S^J^#4L4OhaZ+}eV)EgJG59&Sd zx%;++Q+{zaE?>z{&rtVF%w-`f@!z<{!%`K)9Z%3*KHO4$`}!DP8);^Q)^t-+3au(r zeMubQ_0p@{@Xh@BF|@YXc;TzzGoe9lhoogH`7~#`eGAeWD6?D<7Zb`au{5=-xG=J^ zymV;&jp*(W4+2{KRSAVMDc6#*$U`)*NO$1vJwk#3h=I>!WI9h*Eg%1Ox6jL_*|G4+ zOUm0#W4WDY4l#cvFW2fX&7-p>kUF|qQ6y=eAa})Xm9O(h#^0|x$0Kehhsm@J89dc0 zaqVn9n_@xvI~6>2y4MyhipOu?Z+YTH@;+H;3F)zFH!&w*VEC@x2b5&#ja!`OOOFG( zVw$w_wSA+IlVp`MX56SbRB2Flb!_c^>#`MPp3!I$%$5F{u*8T1IK4`KSfRxyLejJ< zV?ev^*WizF&*(^x%zXuw!XKj0B!XnGuu}A~#oJpZ)us}>O4W*Ed!!IAbXB5O%+w6Ro@ZS!yFZ_BHOMxI~O9|-5Q{9a5k3fYpn9*tyn zATj5ec|F%l6(E2Q2=_7&1-yHzAb3c8B}w|Woe`CY2yhRZo717q=8U8_^I`JdLQ^>K z$tE7&uGWc!WZpq1w?V#fr}4j?RhYaD zB!8R_MQ-Ok;jF8XPPOn2nL5j%fsd2#VvYOi34PICmZDingo;gn-pgs{EJJLB#(Q8T zI}z0XGb&uikz?6hca|7YgB>J$W{n%UYnJnF9!*Q!-iuPGu;`aem#PtFv8n}nF5^7} z+&oZ)(Nf2d+PgMJdUE7@SJYa8%icHKY)Y6|oBjS+y?&`T-&toOtvDed6C7m(fN>cx zl$jY_+JJ*|?1iMp3--)%K9rbx;WV{_ao>`Rq8tbRx%ZPqq9Pm_e3^~uHE}m4yHAz! zGK`vdF>BR@KaN$5Qt6RQ;UWL>Gr*?&uN=nb*>?6*iU)EsTW~ov_~W;cX6E}#T?WxR zp@uqfwuL^4&pKzZHIk35AWNbVHGKLqrN>kz8S7-UE;G!=cXx>-G;v}{Tc zwdc8P-+ij*kd`OK4`%OkNVYgRaRfk=x6MnG`Xn~5sPj|9b0#d=Fwc)!u9(@$T5BNh zn!<>TmYl6#-hya;WYLb!x4xHiK5W;7vgN}rja@gt9Kie0aq+VT`cIrCX=!gv(-NU& zTD{|K$iT`_(F3q!PYuXmU7=2OD$YL2SkYR8D2zO0_yrgTe#d=Lx-FqPv@&zR%j9j< zr(d&oeV#K|YyK0RpL+gh)iv>A%tR$*74dwNdr!eCxt=fS2Z&Wq*7_xvDdPs6QPMS9 zbQtjBISl_cX~BbS#*N9t}ieH-*I;HQcFHD!Zv_w_OLn_)@O*>7RtH&oVm@b zl!IT#wpeFMzBfz$j3(KKBu&&}oMC?but+Guw2?i9378mA>}Oywmrp->og%+crY<@k zq^=OAE--4Hfh7esirPDbWc$7hi^`~8;@P*g14~xhS;s3=-PMhzx{1-GfEqxsZPU!4 z&rHoDuc+1nWGXl2QW(Vtiay>QXb142e!wpk*8dTP6e%KZf8*zWY@Ls*%x*q1MWogG z&-(s*349=@du>ot8@8Q>UzXZ$7Hv`p56YL*Fhfsd1(^LSyVdi1%OY-c&Wevdbcy>I??SJsvzdA>Z;mZ#{vX z!<+~(oYcP8j z!`fG1!y&gV32Sn?y0G|yUN&|E*F1q%-==drOm#0%O2o*Z|D08|9)1BBsnImN(++;+ zOKLA&7A36z6LriC(?0e|t-z!-ZW>RE;@+_C`#f#QA5X{zrUWI0l0=lyZj>xHzT12k ztlb2#^8@7^7O#dSkAj1yG8ysuzZ>viz%xGRDO=jNj-y^GNA235`yNthQ*7)gkBSQB zj;og$qc_Y=RO+?}is2@@nKE%2F=4Yjo1@DF;dP<&y~sb0d(PZ55{TMRd>(s~`aLctp?Ll!-56w6u_s%IkJYu33vQ%BI+)dLBG7sxD1xS| z-ZJ)ej)%NK00Xb7DN+7ul!YX}6U(muXXoTE%-ZAqx@#mLHkPpTS4v<*_?+dPgNkmy zvdqU9kWMAyrR})SD4ASJF?SB1^KP{NMWcT~Pmn8%U#{n(=_K&iu>yImt&^fqt-RZZ zWRY6?v=7D>4c%6GzF0WonW+)WnQ8K*HFL#bJ89&3+vR#{))W`f!XSg^hJx63=#4BN zWz@$-@A9ZqJty3o`HZIffvWmBqY)+hC)W;4Rmy3E$kN6dUZ&O($o zwXmEKu0FKWwk-#(KARQRC;!@W$KPwX>)u>7-y6tIs~*PwR?Y zAm4Kk_Uq)?Nx_12jN-s+pdn^XX)I0LFQzIK6Z5q}3s~f1M-6sU{#t!_TM3a4+1j=Z zhxYda_u)i0=ze~V|62f;S<6=EM#0svk=-b0LUQ{igs))VyH?$s4;rdgl3B^3MUT!F zl}i?0#hN;th=cHWOE=Z14UeZ^} zumVrhfVOdZzvdVJE27~07XBhq82evIrxA}d1-X=1s&%tZ049?IcP-txN0k&Gk#r6} z0N8@yz#30)I$M-jrU-$utG3lzRySon@gn`>iQREnfypc|sB3)2=&RMb@`p#4a!Kb& zb|j~@e#Dk%$*$a3<11kvqxB~lh0$>rRHdVpomsM0f=dCDjx@EJGvC#&*ZGV5#(%qG z5+y_A|8?@Vxwb|uA1A4KARF22$NM-NHPj+&%8PN17OCt?zP9zaPoNPTnJ#&wGSJN& zmHr1V{`6p}qpqvRpLr8Y#ha-#GDa7KM0gFpk#s@?r0-6_xcLB z54Y#3DE0FT1H&_5XaQK{LD>BdWU#F4#1D)g?*h?Cw?f#AzqSOacw$w9V}1L_nV7%3 z{aN@MLK?8_^7q~&NZacRZ{W#9@vTr^+++XFm$0KZY_R>-tP~hUhiYb?$uJjq8wS>d z5_kfaDq8ZUW_(NwhEJsa4mt!8{J;4VpvWq8Z$H#yII_|g|8?*qNb_cz^Tq!5AquF8 z#-KdbL(%IKW!pwP?&al_kM;b`esVLr^g2=Zd3|tt)l~8a5qeD10@U*#mZebxLZmfJ zvv~#4sZ7g_EYC{~BfT<_k^&vg$$AW3_dm#V+OcG0s(_Xjkkei|-=89N86&f{9%D^w zw{y6*tV*y@Q`lcPVT#4IAYc%tvMj@9NrC&^*ugF#I~uGCA9nHI*13d1Z(b{K-D-EhI_;P7 zEH~8(&X(N%_oeZNzrS7#u7zPXqA}w-ztwlL_7>T1X!;^6jR| 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/store/instance.store.ts b/space/store/instance.store.ts index db4fd87a9..c2499d15c 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/root.store.ts b/space/store/root.store.ts index fa0a25aaf..969292ea2 100644 --- a/space/store/root.store.ts +++ b/space/store/root.store.ts @@ -1,52 +1,60 @@ -// 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 { IProfileStore, ProfileStore } from "@/store/user/profile.store"; +import { IUserStore, UserStore } from "@/store/user.store"; -import IssueStore, { IIssueStore } from "./issue"; -import IssueDetailStore, { IIssueDetailStore } from "./issue_details"; -import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store"; -import { IMentionsStore, MentionsStore } from "./mentions.store"; +// import { IProjectStore, ProjectStore } from "@/store/project"; +// import { IProfileStore, ProfileStore } from "@/store/user/profile.store"; + +// import IssueStore, { IIssueStore } from "./issue"; +// import IssueDetailStore, { IIssueDetailStore } from "./issue_details"; +// import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store"; +// import { IMentionsStore, MentionsStore } from "./mentions.store"; enableStaticRendering(typeof window === "undefined"); export class RootStore { instance: IInstanceStore; user: IUserStore; - profile: IProfileStore; - project: IProjectStore; + // profile: IProfileStore; + // project: IProjectStore; - issue: IIssueStore; - issueDetails: IIssueDetailStore; - mentionsStore: IMentionsStore; - issuesFilter: IIssuesFilterStore; + // issue: IIssueStore; + // issueDetails: IIssueDetailStore; + // mentionsStore: IMentionsStore; + // issuesFilter: IIssuesFilterStore; constructor() { + // makeObservable(this, { + // instance: observable, + // }); this.instance = new InstanceStore(this); this.user = new UserStore(this); - this.profile = new ProfileStore(this); - this.project = new ProjectStore(this); + // this.profile = new ProfileStore(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.issue = new IssueStore(this); + // this.issueDetails = new IssueDetailStore(this); + // this.mentionsStore = new MentionsStore(this); + // this.issuesFilter = new IssuesFilterStore(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.profile = new ProfileStore(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.profile = new ProfileStore(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); }; } diff --git a/space/store/user.store.ts b/space/store/user.store.ts new file mode 100644 index 000000000..7c37bc2c2 --- /dev/null +++ b/space/store/user.store.ts @@ -0,0 +1,183 @@ +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +// types +import { IUser } from "@plane/types"; +// services +import { AuthService } from "@/services/authentication.service"; +import { UserService } from "@/services/user.service"; +// stores +import { RootStore } from "@/store/root.store"; +import { ProfileStore, IProfileStore } from "@/store/user/profile.store"; +import { ActorDetail } from "@/types/issue"; + +type TUserErrorStatus = { + status: string; + message: string; +}; + +export interface IUserStore { + // observables + isAuthenticated: boolean; + isLoading: boolean; + error: TUserErrorStatus | undefined; + data: IUser | undefined; + // store observables + profile: IProfileStore; + // computed + currentActor: ActorDetail; + // actions + fetchCurrentUser: () => Promise; + updateCurrentUser: (data: Partial) => Promise; + hydrate: (data: IUser) => void; + reset: () => void; + signOut: () => Promise; +} + +export class UserStore implements IUserStore { + // observables + isAuthenticated: boolean = false; + isLoading: boolean = false; + error: TUserErrorStatus | undefined = undefined; + data: IUser | undefined = undefined; + // store observables + profile: IProfileStore; + // service + userService: UserService; + authService: AuthService; + + constructor(private store: RootStore) { + // stores + this.profile = new ProfileStore(store); + // service + this.userService = new UserService(); + this.authService = new AuthService(); + // observables + makeObservable(this, { + // observables + isAuthenticated: observable.ref, + isLoading: observable.ref, + error: observable, + // model observables + data: observable, + profile: observable, + // computed + currentActor: computed, + // actions + fetchCurrentUser: action, + updateCurrentUser: action, + reset: action, + signOut: action, + }); + } + + // computed + get currentActor(): ActorDetail { + return { + id: this.data?.id, + first_name: this.data?.first_name, + last_name: this.data?.last_name, + display_name: this.data?.display_name, + avatar: this.data?.avatar || undefined, + is_bot: false, + }; + } + + // actions + /** + * @description fetches the current user + * @returns {Promise} + */ + fetchCurrentUser = async (): Promise => { + try { + runInAction(() => { + this.isLoading = true; + this.error = undefined; + }); + const user = await this.userService.currentUser(); + if (user && user?.id) { + await this.profile.fetchUserProfile(); + runInAction(() => { + this.data = user; + this.isLoading = false; + this.isAuthenticated = true; + }); + } else + runInAction(() => { + this.data = user; + this.isLoading = false; + this.isAuthenticated = false; + }); + return user; + } catch (error) { + runInAction(() => { + this.isLoading = false; + this.isAuthenticated = false; + this.error = { + status: "user-fetch-error", + message: "Failed to fetch current user", + }; + }); + throw error; + } + }; + + /** + * @description updates the current user + * @param data + * @returns {Promise} + */ + updateCurrentUser = async (data: Partial): Promise => { + const currentUserData = this.data; + try { + if (currentUserData) { + Object.keys(data).forEach((key: string) => { + const userKey: keyof IUser = key as keyof IUser; + if (this.data) set(this.data, userKey, data[userKey]); + }); + } + const user = await this.userService.updateUser(data); + return user; + } catch (error) { + if (currentUserData) { + Object.keys(currentUserData).forEach((key: string) => { + const userKey: keyof IUser = key as keyof IUser; + if (this.data) set(this.data, userKey, currentUserData[userKey]); + }); + } + runInAction(() => { + this.error = { + status: "user-update-error", + message: "Failed to update current user", + }; + }); + throw error; + } + }; + + hydrate = (data: IUser): void => { + this.data = { ...this.data, ...data }; + }; + + /** + * @description resets the user store + * @returns {void} + */ + reset = (): void => { + runInAction(() => { + this.isAuthenticated = false; + this.isLoading = false; + this.error = undefined; + this.data = undefined; + this.profile = new ProfileStore(this.store); + }); + }; + + /** + * @description signs out the current user + * @returns {Promise} + */ + signOut = async (): Promise => { + // await this.authService.signOut(API_BASE_URL); + // this.store.resetOnSignOut(); + }; +} 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" + } + ] } } From 55a173d3e35496421b62ab2c756051085395a5be Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 13 May 2024 23:22:26 +0530 Subject: [PATCH 2/2] fix: space app dir fixes --- .../[workspace_slug]/[project_id]/error.tsx | 5 + .../[workspace_slug]/[project_id]/layout.tsx | 38 ++++ .../[workspace_slug]/[project_id]/page.tsx | 8 + .../[project_identifier]/layout.tsx | 30 --- .../[project_identifier]/page.tsx | 44 ----- space/app/page.tsx | 35 ++-- .../components/accounts/auth-forms/email.tsx | 2 + .../auth-forms/forgot-password-popover.tsx | 1 + .../accounts/auth-forms/password.tsx | 10 +- space/components/accounts/auth-forms/root.tsx | 14 +- .../accounts/auth-forms/unique-code.tsx | 6 +- .../accounts/oauth/github-button.tsx | 2 + .../accounts/oauth/google-button.tsx | 2 + .../accounts/oauth/oauth-options.tsx | 8 +- space/components/accounts/onboarding-form.tsx | 8 +- .../accounts/password-strength-meter.tsx | 2 + .../accounts/terms-and-conditions.tsx | 2 + .../accounts/user-image-upload-modal.tsx | 5 +- space/components/accounts/user-logged-in.tsx | 2 + .../issues/board-views/kanban/block.tsx | 2 +- .../issues/board-views/list/block.tsx | 2 +- .../issues/board-views/list/index.tsx | 21 +-- .../issues/filters/applied-filters/root.tsx | 45 +++-- space/components/issues/filters/root.tsx | 2 +- space/components/issues/navbar/index.tsx | 168 ++++++++--------- .../issues/navbar/issue-board-view.tsx | 46 ++--- space/components/issues/navbar/theme.tsx | 3 +- .../peek-overview/comment/add-comment.tsx | 2 +- .../comment/comment-reactions.tsx | 2 +- .../peek-overview/full-screen-peek-view.tsx | 2 +- .../issues/peek-overview/header.tsx | 2 +- .../issues/peek-overview/issue-activity.tsx | 2 +- .../peek-overview/issue-emoji-reactions.tsx | 27 ++- .../issues/peek-overview/issue-properties.tsx | 2 +- .../issues/peek-overview/issue-reaction.tsx | 18 +- .../peek-overview/issue-vote-reactions.tsx | 21 +-- .../issues/peek-overview/layout.tsx | 42 ++--- space/components/views/auth.tsx | 45 +++-- space/components/views/project-details.tsx | 61 +++--- space/constants/issue.ts | 20 ++ space/hooks/store/index.ts | 7 +- space/hooks/store/use-instance.ts | 5 +- space/hooks/store/use-issue-details.tsx | 11 ++ space/hooks/store/use-issue-filter.ts | 11 ++ space/hooks/store/use-issue.ts | 11 ++ space/hooks/store/use-project.ts | 11 ++ .../store/{user => }/use-user-profile.ts | 5 +- space/hooks/store/{user => }/use-user.ts | 0 space/hooks/store/user/index.ts | 2 - space/layouts/project-layout.tsx | 32 ---- ...hentication.service.ts => auth.service.ts} | 0 space/services/issue.service.ts | 2 +- space/services/project.service.ts | 2 +- space/store/instance.store.ts | 4 +- ...issue_details.ts => issue-detail.store.ts} | 4 +- .../store/{issues => }/issue-filters.store.ts | 67 ++++--- space/store/{issue.ts => issue.store.ts} | 22 +-- space/store/issues/base-issue-filter.store.ts | 29 --- space/store/issues/helpers.ts | 52 ----- space/store/issues/types.ts | 36 ---- space/store/{user => }/profile.store.ts | 0 space/store/{project.ts => project.store.ts} | 65 +++++-- space/store/root.store.ts | 51 ++--- space/store/user.store.ts | 4 +- space/store/user/index.ts | 178 ------------------ space/types/issue-filters.d.ts | 6 + space/types/issue.d.ts | 35 ++++ 67 files changed, 613 insertions(+), 798 deletions(-) create mode 100644 space/app/[workspace_slug]/[project_id]/error.tsx create mode 100644 space/app/[workspace_slug]/[project_id]/layout.tsx create mode 100644 space/app/[workspace_slug]/[project_id]/page.tsx delete mode 100644 space/app/[workspace_slug]/[project_identifier]/layout.tsx delete mode 100644 space/app/[workspace_slug]/[project_identifier]/page.tsx create mode 100644 space/constants/issue.ts create mode 100644 space/hooks/store/use-issue-details.tsx create mode 100644 space/hooks/store/use-issue-filter.ts create mode 100644 space/hooks/store/use-issue.ts create mode 100644 space/hooks/store/use-project.ts rename space/hooks/store/{user => }/use-user-profile.ts (84%) rename space/hooks/store/{user => }/use-user.ts (100%) delete mode 100644 space/hooks/store/user/index.ts delete mode 100644 space/layouts/project-layout.tsx rename space/services/{authentication.service.ts => auth.service.ts} (100%) rename space/store/{issue_details.ts => issue-detail.store.ts} (99%) rename space/store/{issues => }/issue-filters.store.ts (55%) rename space/store/{issue.ts => issue.store.ts} (82%) delete mode 100644 space/store/issues/base-issue-filter.store.ts delete mode 100644 space/store/issues/helpers.ts delete mode 100644 space/store/issues/types.ts rename space/store/{user => }/profile.store.ts (100%) rename space/store/{project.ts => project.store.ts} (57%) delete mode 100644 space/store/user/index.ts create mode 100644 space/types/issue-filters.d.ts 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 ( +

+ ); +} 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/[workspace_slug]/[project_identifier]/layout.tsx b/space/app/[workspace_slug]/[project_identifier]/layout.tsx deleted file mode 100644 index d831fd234..000000000 --- a/space/app/[workspace_slug]/[project_identifier]/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"; -// assets -import planeLogo from "public/plane-logo.svg"; - -const ProjectLayout = ({ children }: { children: React.ReactNode }) => ( - -); - -export default observer(ProjectLayout); diff --git a/space/app/[workspace_slug]/[project_identifier]/page.tsx b/space/app/[workspace_slug]/[project_identifier]/page.tsx deleted file mode 100644 index 44ecd0aab..000000000 --- a/space/app/[workspace_slug]/[project_identifier]/page.tsx +++ /dev/null @@ -1,44 +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 ( - - - - - - ); -}; - -export default WorkspaceProjectPage; diff --git a/space/app/page.tsx b/space/app/page.tsx index ddf10a287..bdd42c3af 100644 --- a/space/app/page.tsx +++ b/space/app/page.tsx @@ -1,20 +1,31 @@ -"use client"; // components +import { UserLoggedIn } from "@/components/accounts"; import { AuthView } from "@/components/views"; // helpers -import { EPageTypes } from "@/helpers/authentication.helper"; -import { useInstance } from "@/hooks/store"; +// import { EPageTypes } from "@/helpers/authentication.helper"; +// import { useInstance, useUser } from "@/hooks/store"; // wrapper -import { AuthWrapper } from "@/lib/wrappers"; +// import { AuthWrapper } from "@/lib/wrappers"; +// services +import { UserService } from "@/services/user.service"; -export default function HomePage() { - const { data } = useInstance(); +const userServices = new UserService(); - console.log("data", data); +export default async function HomePage() { + const user = await userServices + .currentUser() + .then((user) => ({ ...user, isAuthenticated: true })) + .catch(() => ({ isAuthenticated: false })); - return ( - - - - ); + // 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 7a719d938..fa2e9afd5 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/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 ? ( - + ) : ( @@ -117,16 +110,13 @@ const IssueNavbar = observer(() => { )}
- {projectStore?.project?.name || `...`} + {project_details?.name || `...`}
- {/* issue search bar */} -
{/* */}
- {/* issue views */}
- +
{/* issue filters */} @@ -139,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 29dfa5680..87ff8c366 100644 --- a/space/components/views/auth.tsx +++ b/space/components/views/auth.tsx @@ -7,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 @@ -19,12 +19,15 @@ export const AuthView = observer(() => { // hooks const { resolvedTheme } = useTheme(); // store - const { fetchCurrentUser, isLoading, isAuthenticated } = 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 ( @@ -35,30 +38,26 @@ export const AuthView = observer(() => {
) : ( <> - {isAuthenticated ? ( - - ) : ( -
-
- 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/hooks/store/index.ts b/space/hooks/store/index.ts index 9a48d10f2..76b6f9315 100644 --- a/space/hooks/store/index.ts +++ b/space/hooks/store/index.ts @@ -1,2 +1,7 @@ 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 15fdaf84f..62aa0baae 100644 --- a/space/hooks/store/use-instance.ts +++ b/space/hooks/store/use-instance.ts @@ -1,10 +1,11 @@ import { useContext } from "react"; -// store +// lib import { StoreContext } from "@/lib/app-providers"; +// store 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 84% rename from space/hooks/store/user/use-user-profile.ts rename to space/hooks/store/use-user-profile.ts index 61bfe64f8..042f16c0d 100644 --- a/space/hooks/store/user/use-user-profile.ts +++ b/space/hooks/store/use-user-profile.ts @@ -1,7 +1,8 @@ import { useContext } from "react"; -// store +// lib import { StoreContext } from "@/lib/app-providers"; -import { IProfileStore } from "@/store/user/profile.store"; +// store +import { IProfileStore } from "@/store/profile.store"; export const useUserProfile = (): IProfileStore => { const context = useContext(StoreContext); diff --git a/space/hooks/store/user/use-user.ts b/space/hooks/store/use-user.ts similarity index 100% rename from space/hooks/store/user/use-user.ts rename to space/hooks/store/use-user.ts 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 c5946277f..000000000 --- a/space/layouts/project-layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import Image from "next/image"; - -// mobx -import { observer } from "mobx-react-lite"; -import planeLogo from "public/plane-logo.svg"; -// components -import IssueNavbar from "@/components/issues/navbar"; - -const ProjectLayout = ({ children }: { children: React.ReactNode }) => ( - -); - -export default observer(ProjectLayout); 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 c2499d15c..4a410d851 100644 --- a/space/store/instance.store.ts +++ b/space/store/instance.store.ts @@ -19,7 +19,7 @@ export interface IInstanceStore { // issues isLoading: boolean; data: IInstance | NonNullable; - config: Record; + config: Record; error: TError | undefined; // action fetchInstanceInfo: () => Promise; @@ -28,7 +28,7 @@ export interface IInstanceStore { export class InstanceStore implements IInstanceStore { isLoading: boolean = true; - data: IInstance | Record = {}; + data: IInstance | Record = {}; config: Record = {}; error: TError | undefined = undefined; // services 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 969292ea2..8b6b10f51 100644 --- a/space/store/root.store.ts +++ b/space/store/root.store.ts @@ -1,42 +1,32 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports import { IInstanceStore, InstanceStore } from "@/store/instance.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 { IProjectStore, ProjectStore } from "@/store/project"; -// import { IProfileStore, ProfileStore } from "@/store/user/profile.store"; - -// import IssueStore, { IIssueStore } from "./issue"; -// import IssueDetailStore, { IIssueDetailStore } from "./issue_details"; -// import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store"; -// import { IMentionsStore, MentionsStore } from "./mentions.store"; +import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store"; +import { IMentionsStore, MentionsStore } from "./mentions.store"; enableStaticRendering(typeof window === "undefined"); export class RootStore { instance: IInstanceStore; user: IUserStore; - // profile: IProfileStore; - // project: IProjectStore; - - // issue: IIssueStore; - // issueDetails: IIssueDetailStore; - // mentionsStore: IMentionsStore; - // issuesFilter: IIssuesFilterStore; + project: IProjectStore; + issue: IIssueStore; + issueDetail: IIssueDetailStore; + mentionStore: IMentionsStore; + issueFilter: IIssueFilterStore; constructor() { - // makeObservable(this, { - // instance: observable, - // }); this.instance = new InstanceStore(this); this.user = new UserStore(this); - // this.profile = new ProfileStore(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.project = new ProjectStore(this); + this.issue = new IssueStore(this); + this.issueDetail = new IssueDetailStore(this); + this.mentionStore = new MentionsStore(this); + this.issueFilter = new IssueFilterStore(this); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -50,11 +40,10 @@ export class RootStore { localStorage.setItem("theme", "system"); this.instance = new InstanceStore(this); this.user = new UserStore(this); - // this.profile = new ProfileStore(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.project = new ProjectStore(this); + this.issue = new IssueStore(this); + this.issueDetail = new IssueDetailStore(this); + this.mentionStore = new MentionsStore(this); + this.issueFilter = new IssueFilterStore(this); }; } diff --git a/space/store/user.store.ts b/space/store/user.store.ts index 7c37bc2c2..2f228b629 100644 --- a/space/store/user.store.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 = { diff --git a/space/store/user/index.ts b/space/store/user/index.ts deleted file mode 100644 index e5bfc41c6..000000000 --- a/space/store/user/index.ts +++ /dev/null @@ -1,178 +0,0 @@ -import set from "lodash/set"; -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -// types -import { IUser } from "@plane/types"; -// services -import { AuthService } from "@/services/authentication.service"; -import { UserService } from "@/services/user.service"; -// stores -import { RootStore } from "@/store/root.store"; -import { ProfileStore, IProfileStore } from "@/store/user/profile.store"; -import { ActorDetail } from "@/types/issue"; - -type TUserErrorStatus = { - status: string; - message: string; -}; - -export interface IUserStore { - // observables - isAuthenticated: boolean; - isLoading: boolean; - error: TUserErrorStatus | undefined; - data: IUser | undefined; - // store observables - userProfile: IProfileStore; - // computed - currentActor: ActorDetail; - // actions - fetchCurrentUser: () => Promise; - updateCurrentUser: (data: Partial) => Promise; - reset: () => void; - signOut: () => Promise; -} - -export class UserStore implements IUserStore { - // observables - isAuthenticated: boolean = false; - isLoading: boolean = false; - error: TUserErrorStatus | undefined = undefined; - data: IUser | undefined = undefined; - // store observables - userProfile: IProfileStore; - // service - userService: UserService; - authService: AuthService; - - constructor(private store: RootStore) { - // stores - this.userProfile = new ProfileStore(store); - // service - this.userService = new UserService(); - this.authService = new AuthService(); - // observables - makeObservable(this, { - // observables - isAuthenticated: observable.ref, - isLoading: observable.ref, - error: observable, - // model observables - data: observable, - userProfile: observable, - // computed - currentActor: computed, - // actions - fetchCurrentUser: action, - updateCurrentUser: action, - reset: action, - signOut: action, - }); - } - - // computed - get currentActor(): ActorDetail { - return { - id: this.data?.id, - first_name: this.data?.first_name, - last_name: this.data?.last_name, - display_name: this.data?.display_name, - avatar: this.data?.avatar || undefined, - is_bot: false, - }; - } - - // actions - /** - * @description fetches the current user - * @returns {Promise} - */ - fetchCurrentUser = async (): Promise => { - try { - runInAction(() => { - this.isLoading = true; - this.error = undefined; - }); - const user = await this.userService.currentUser(); - if (user && user?.id) { - await this.userProfile.fetchUserProfile(); - runInAction(() => { - this.data = user; - this.isLoading = false; - this.isAuthenticated = true; - }); - } else - runInAction(() => { - this.data = user; - this.isLoading = false; - this.isAuthenticated = false; - }); - return user; - } catch (error) { - runInAction(() => { - this.isLoading = false; - this.isAuthenticated = false; - this.error = { - status: "user-fetch-error", - message: "Failed to fetch current user", - }; - }); - throw error; - } - }; - - /** - * @description updates the current user - * @param data - * @returns {Promise} - */ - updateCurrentUser = async (data: Partial): Promise => { - const currentUserData = this.data; - try { - if (currentUserData) { - Object.keys(data).forEach((key: string) => { - const userKey: keyof IUser = key as keyof IUser; - if (this.data) set(this.data, userKey, data[userKey]); - }); - } - const user = await this.userService.updateUser(data); - return user; - } catch (error) { - if (currentUserData) { - Object.keys(currentUserData).forEach((key: string) => { - const userKey: keyof IUser = key as keyof IUser; - if (this.data) set(this.data, userKey, currentUserData[userKey]); - }); - } - runInAction(() => { - this.error = { - status: "user-update-error", - message: "Failed to update current user", - }; - }); - throw error; - } - }; - - /** - * @description resets the user store - * @returns {void} - */ - reset = (): void => { - runInAction(() => { - this.isAuthenticated = false; - this.isLoading = false; - this.error = undefined; - this.data = undefined; - this.userProfile = new ProfileStore(this.store); - }); - }; - - /** - * @description signs out the current user - * @returns {Promise} - */ - signOut = async (): Promise => { - // await this.authService.signOut(API_BASE_URL); - // this.store.resetOnSignOut(); - }; -} 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; +}