diff --git a/.eslintrc.js b/.eslintrc.js index c229c09526..b1a019e351 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { extends: ["custom"], settings: { next: { - rootDir: ["web/", "space/"], + rootDir: ["web/", "space/", "admin/"], }, }, }; diff --git a/admin/.env.example b/admin/.env.example index a86a8b4fba..fdeb05c4d7 100644 --- a/admin/.env.example +++ b/admin/.env.example @@ -1,5 +1,3 @@ NEXT_PUBLIC_API_BASE_URL="" -NEXT_PUBLIC_ADMIN_BASE_URL="" -NEXT_PUBLIC_SPACE_BASE_URL="" -NEXT_PUBLIC_WEB_BASE_URL="" -NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" \ No newline at end of file +NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" +NEXT_PUBLIC_WEB_BASE_URL="" \ No newline at end of file diff --git a/admin/Dockerfile.admin b/admin/Dockerfile.admin index 901c39e27d..b2908f356c 100644 --- a/admin/Dockerfile.admin +++ b/admin/Dockerfile.admin @@ -32,7 +32,7 @@ ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ARG NEXT_PUBLIC_WEB_BASE_URL="" ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL -ARG NEXT_PUBLIC_SPACE_BASE_URL="" +ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces" ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" @@ -62,7 +62,7 @@ ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ARG NEXT_PUBLIC_WEB_BASE_URL="" ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL -ARG NEXT_PUBLIC_SPACE_BASE_URL="" +ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces" ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" diff --git a/admin/app/ai/components/ai-config-form.tsx b/admin/app/ai/components/ai-config-form.tsx index d61eb9ed94..fda70611c2 100644 --- a/admin/app/ai/components/ai-config-form.tsx +++ b/admin/app/ai/components/ai-config-form.tsx @@ -6,7 +6,7 @@ import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@ // components import { ControllerInput, TControllerInputFormField } from "components/common"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; type IInstanceAIForm = { config: IFormattedInstanceConfiguration; diff --git a/admin/app/ai/page.tsx b/admin/app/ai/page.tsx index 71af4a5ba9..5d002ca55b 100644 --- a/admin/app/ai/page.tsx +++ b/admin/app/ai/page.tsx @@ -7,7 +7,7 @@ import { Loader } from "@plane/ui"; import { PageHeader } from "@/components/core"; import { InstanceAIForm } from "./components"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; const InstanceAIPage = observer(() => { // store diff --git a/admin/app/authentication/components/email-config-switch.tsx b/admin/app/authentication/components/email-config-switch.tsx index 0958b3c424..9c23901fef 100644 --- a/admin/app/authentication/components/email-config-switch.tsx +++ b/admin/app/authentication/components/email-config-switch.tsx @@ -3,7 +3,7 @@ import React from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // ui import { ToggleSwitch } from "@plane/ui"; // types diff --git a/admin/app/authentication/components/password-config-switch.tsx b/admin/app/authentication/components/password-config-switch.tsx index 92428e494d..ce33cd3294 100644 --- a/admin/app/authentication/components/password-config-switch.tsx +++ b/admin/app/authentication/components/password-config-switch.tsx @@ -3,7 +3,7 @@ import React from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // ui import { ToggleSwitch } from "@plane/ui"; // types diff --git a/admin/app/authentication/github/components/github-config-form.tsx b/admin/app/authentication/github/components/github-config-form.tsx index 22eb11ff4e..43d2205757 100644 --- a/admin/app/authentication/github/components/github-config-form.tsx +++ b/admin/app/authentication/github/components/github-config-form.tsx @@ -2,7 +2,7 @@ import { FC, useState } from "react"; import { useForm } from "react-hook-form"; import Link from "next/link"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // ui import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; // components diff --git a/admin/app/authentication/github/components/root.tsx b/admin/app/authentication/github/components/root.tsx index 742462c3b9..d820bc8a24 100644 --- a/admin/app/authentication/github/components/root.tsx +++ b/admin/app/authentication/github/components/root.tsx @@ -4,7 +4,7 @@ import React from "react"; import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // ui import { ToggleSwitch, getButtonStyling } from "@plane/ui"; // icons diff --git a/admin/app/authentication/github/page.tsx b/admin/app/authentication/github/page.tsx index 6470f812af..893762d472 100644 --- a/admin/app/authentication/github/page.tsx +++ b/admin/app/authentication/github/page.tsx @@ -11,7 +11,7 @@ import { PageHeader } from "@/components/core"; import { AuthenticationMethodCard } from "../components"; import { InstanceGithubConfigForm } from "./components"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // helpers import { resolveGeneralTheme } from "@/helpers/common.helper"; // icons diff --git a/admin/app/authentication/google/components/google-config-form.tsx b/admin/app/authentication/google/components/google-config-form.tsx index 42cea78fd5..f070216940 100644 --- a/admin/app/authentication/google/components/google-config-form.tsx +++ b/admin/app/authentication/google/components/google-config-form.tsx @@ -2,7 +2,7 @@ import { FC, useState } from "react"; import { useForm } from "react-hook-form"; import Link from "next/link"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // ui import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; // components diff --git a/admin/app/authentication/google/components/root.tsx b/admin/app/authentication/google/components/root.tsx index 6b287476dd..5432c95bf9 100644 --- a/admin/app/authentication/google/components/root.tsx +++ b/admin/app/authentication/google/components/root.tsx @@ -4,7 +4,7 @@ import React from "react"; import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // ui import { ToggleSwitch, getButtonStyling } from "@plane/ui"; // icons diff --git a/admin/app/authentication/google/page.tsx b/admin/app/authentication/google/page.tsx index f7fa6e6434..9b02842afe 100644 --- a/admin/app/authentication/google/page.tsx +++ b/admin/app/authentication/google/page.tsx @@ -10,7 +10,7 @@ import { PageHeader } from "@/components/core"; import { AuthenticationMethodCard } from "../components"; import { InstanceGoogleConfigForm } from "./components"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // icons import GoogleLogo from "@/public/logos/google-logo.svg"; diff --git a/admin/app/authentication/page.tsx b/admin/app/authentication/page.tsx index 59e4056082..0685924683 100644 --- a/admin/app/authentication/page.tsx +++ b/admin/app/authentication/page.tsx @@ -14,7 +14,7 @@ import { GoogleConfiguration } from "./google/components"; import { GithubConfiguration } from "./github/components"; import { PageHeader } from "@/components/core"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // helpers import { resolveGeneralTheme } from "@/helpers/common.helper"; // images diff --git a/admin/app/email/components/email-config-form.tsx b/admin/app/email/components/email-config-form.tsx index 38b50d50f7..50c8671325 100644 --- a/admin/app/email/components/email-config-form.tsx +++ b/admin/app/email/components/email-config-form.tsx @@ -1,7 +1,7 @@ import React, { FC, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // ui import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; // components diff --git a/admin/app/email/page.tsx b/admin/app/email/page.tsx index a3b0bed596..6ffebc904d 100644 --- a/admin/app/email/page.tsx +++ b/admin/app/email/page.tsx @@ -7,7 +7,7 @@ import { Loader } from "@plane/ui"; import { PageHeader } from "@/components/core"; import { InstanceEmailForm } from "./components"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; const InstanceEmailPage = observer(() => { // store diff --git a/admin/app/general/components/general-config-form.tsx b/admin/app/general/components/general-config-form.tsx index f45876419c..5e360e0481 100644 --- a/admin/app/general/components/general-config-form.tsx +++ b/admin/app/general/components/general-config-form.tsx @@ -6,7 +6,7 @@ import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // components import { ControllerInput } from "components/common"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; export interface IGeneralConfigurationForm { instance: IInstance["instance"]; diff --git a/admin/app/general/page.tsx b/admin/app/general/page.tsx index 10429c1c9a..accaf01d12 100644 --- a/admin/app/general/page.tsx +++ b/admin/app/general/page.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite"; import { PageHeader } from "@/components/core"; import { GeneralConfigurationForm } from "./components"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; const GeneralPage = observer(() => { const { instance, instanceAdmins } = useInstance(); diff --git a/admin/app/image/components/image-config-form.tsx b/admin/app/image/components/image-config-form.tsx index 722051878b..1779468fa8 100644 --- a/admin/app/image/components/image-config-form.tsx +++ b/admin/app/image/components/image-config-form.tsx @@ -5,7 +5,7 @@ import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from // components import { ControllerInput } from "components/common"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; type IInstanceImageConfigForm = { config: IFormattedInstanceConfiguration; diff --git a/admin/app/image/page.tsx b/admin/app/image/page.tsx index 68572c519f..cbf4a8f4d2 100644 --- a/admin/app/image/page.tsx +++ b/admin/app/image/page.tsx @@ -7,7 +7,7 @@ import { Loader } from "@plane/ui"; import { PageHeader } from "@/components/core"; import { InstanceImageConfigForm } from "./components"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; const InstanceImagePage = observer(() => { // store diff --git a/admin/app/layout.tsx b/admin/app/layout.tsx index d991f9d82c..3352cbfaec 100644 --- a/admin/app/layout.tsx +++ b/admin/app/layout.tsx @@ -7,6 +7,8 @@ import { StoreProvider } from "@/lib/store-context"; import { AppWrapper } from "@/lib/wrappers"; // constants import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo"; +// helpers +import { ASSET_PREFIX } from "@/helpers/common.helper"; // styles import "./globals.css"; @@ -14,35 +16,31 @@ interface RootLayoutProps { children: ReactNode; } -const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => { - const prefix = "/god-mode/"; - - return ( - - - {SITE_TITLE} - - - - - - - - - - - - - - - - - {children} - - - - - ); -}; +const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => ( + + + {SITE_TITLE} + + + + + + + + + + + + + + + + + {children} + + + + +); export default RootLayout; diff --git a/admin/components/admin-sidebar/help-section.tsx b/admin/components/admin-sidebar/help-section.tsx index ba8f2cba55..8b3f5baeb1 100644 --- a/admin/components/admin-sidebar/help-section.tsx +++ b/admin/components/admin-sidebar/help-section.tsx @@ -7,7 +7,7 @@ import { Transition } from "@headlessui/react"; import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react"; import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // hooks -import { useInstance, useTheme } from "@/hooks"; +import { useInstance, useTheme } from "@/hooks/store"; // assets import packageJson from "package.json"; diff --git a/admin/components/admin-sidebar/root.tsx b/admin/components/admin-sidebar/root.tsx index 3b754d8b2f..6547699244 100644 --- a/admin/components/admin-sidebar/root.tsx +++ b/admin/components/admin-sidebar/root.tsx @@ -3,7 +3,7 @@ import { FC, useEffect, useRef } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useTheme } from "@/hooks"; +import { useTheme } from "@/hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar"; diff --git a/admin/components/admin-sidebar/sidebar-dropdown.tsx b/admin/components/admin-sidebar/sidebar-dropdown.tsx index 68212464e6..f248f852f1 100644 --- a/admin/components/admin-sidebar/sidebar-dropdown.tsx +++ b/admin/components/admin-sidebar/sidebar-dropdown.tsx @@ -7,7 +7,7 @@ import { LogOut, UserCog2, Palette } from "lucide-react"; import { Menu, Transition } from "@headlessui/react"; import { Avatar } from "@plane/ui"; // hooks -import { useTheme, useUser } from "@/hooks"; +import { useTheme, useUser } from "@/hooks/store"; // helpers import { API_BASE_URL, cn } from "@/helpers/common.helper"; // services diff --git a/admin/components/admin-sidebar/sidebar-menu-hamburger-toogle.tsx b/admin/components/admin-sidebar/sidebar-menu-hamburger-toogle.tsx index ba00afa7f6..d6ed655411 100644 --- a/admin/components/admin-sidebar/sidebar-menu-hamburger-toogle.tsx +++ b/admin/components/admin-sidebar/sidebar-menu-hamburger-toogle.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useTheme } from "@/hooks"; +import { useTheme } from "@/hooks/store"; // icons import { Menu } from "lucide-react"; diff --git a/admin/components/admin-sidebar/sidebar-menu.tsx b/admin/components/admin-sidebar/sidebar-menu.tsx index e7111aea22..dfb4100511 100644 --- a/admin/components/admin-sidebar/sidebar-menu.tsx +++ b/admin/components/admin-sidebar/sidebar-menu.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite"; import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react"; import { Tooltip } from "@plane/ui"; // hooks -import { useTheme } from "@/hooks"; +import { useTheme } from "@/hooks/store"; // helpers import { cn } from "@/helpers/common.helper"; diff --git a/admin/components/common/empty-state.tsx b/admin/components/common/empty-state.tsx new file mode 100644 index 0000000000..fbbe0bc0f3 --- /dev/null +++ b/admin/components/common/empty-state.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import Image from "next/image"; +import { Button } from "@plane/ui"; + +type Props = { + title: string; + description?: React.ReactNode; + image?: any; + primaryButton?: { + icon?: any; + text: string; + onClick: () => void; + }; + secondaryButton?: React.ReactNode; + disabled?: boolean; +}; + +export const EmptyState: React.FC = ({ + title, + description, + image, + primaryButton, + secondaryButton, + disabled = false, +}) => ( +
+
+ {image && {primaryButton?.text} +
{title}
+ {description &&

{description}

} +
+ {primaryButton && ( + + )} + {secondaryButton} +
+
+
+); diff --git a/admin/components/common/index.ts b/admin/components/common/index.ts index 97248b999d..77f0e9327f 100644 --- a/admin/components/common/index.ts +++ b/admin/components/common/index.ts @@ -4,3 +4,4 @@ export * from "./controller-input"; export * from "./copy-field"; export * from "./password-strength-meter"; export * from "./banner"; +export * from "./empty-state"; diff --git a/admin/components/new-user-popup.tsx b/admin/components/new-user-popup.tsx index d17e99d5ea..6b4cea340c 100644 --- a/admin/components/new-user-popup.tsx +++ b/admin/components/new-user-popup.tsx @@ -9,7 +9,7 @@ import { Button, getButtonStyling } from "@plane/ui"; // helpers import { resolveGeneralTheme } from "helpers/common.helper"; // hooks -import { useInstance, useTheme } from "@/hooks"; +import { useInstance, useTheme } from "@/hooks/store"; // icons import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg"; import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg"; diff --git a/admin/helpers/common.helper.ts b/admin/helpers/common.helper.ts index 3bf03024bb..e7aae0698f 100644 --- a/admin/helpers/common.helper.ts +++ b/admin/helpers/common.helper.ts @@ -1,7 +1,16 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; -export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : ""; +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || ""; + +export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || ""; + +export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || ""; +export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || ""; + +export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || ""; + +export const ASSET_PREFIX = ADMIN_BASE_PATH; export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); diff --git a/admin/hooks/index.ts b/admin/hooks/index.ts deleted file mode 100644 index 273970eda1..0000000000 --- a/admin/hooks/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./use-outside-click-detector"; - -// store-hooks -export * from "./store/use-theme"; -export * from "./store/use-instance"; -export * from "./store/use-user"; diff --git a/admin/hooks/store/index.ts b/admin/hooks/store/index.ts new file mode 100644 index 0000000000..7447064da7 --- /dev/null +++ b/admin/hooks/store/index.ts @@ -0,0 +1,3 @@ +export * from "./use-theme"; +export * from "./use-instance"; +export * from "./use-user"; diff --git a/admin/lib/wrappers/app-wrapper.tsx b/admin/lib/wrappers/app-wrapper.tsx index 6be1cec246..aa6e263305 100644 --- a/admin/lib/wrappers/app-wrapper.tsx +++ b/admin/lib/wrappers/app-wrapper.tsx @@ -4,11 +4,11 @@ import { FC, ReactNode, useEffect, Suspense } from "react"; import { observer } from "mobx-react-lite"; import { SWRConfig } from "swr"; // hooks -import { useTheme, useUser } from "@/hooks"; +import { useTheme, useUser } from "@/hooks/store"; // ui import { Toast } from "@plane/ui"; // constants -import { SWR_CONFIG } from "constants/swr-config"; +import { SWR_CONFIG } from "@/constants/swr-config"; // helpers import { resolveGeneralTheme } from "helpers/common.helper"; diff --git a/admin/lib/wrappers/auth-wrapper.tsx b/admin/lib/wrappers/auth-wrapper.tsx index 75e7c2accd..00f947047b 100644 --- a/admin/lib/wrappers/auth-wrapper.tsx +++ b/admin/lib/wrappers/auth-wrapper.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite"; import useSWR from "swr"; import { Spinner } from "@plane/ui"; // hooks -import { useInstance, useUser } from "@/hooks"; +import { useInstance, useUser } from "@/hooks/store"; // helpers import { EAuthenticationPageType } from "@/helpers"; diff --git a/admin/lib/wrappers/instance-wrapper.tsx b/admin/lib/wrappers/instance-wrapper.tsx index da02992aa3..f86adfdce5 100644 --- a/admin/lib/wrappers/instance-wrapper.tsx +++ b/admin/lib/wrappers/instance-wrapper.tsx @@ -10,9 +10,10 @@ import { DefaultLayout } from "@/layouts"; // components import { InstanceNotReady } from "@/components/instance"; // hooks -import { useInstance } from "@/hooks"; +import { useInstance } from "@/hooks/store"; // helpers import { EInstancePageType } from "@/helpers"; +import { EmptyState } from "@/components/common"; type TInstanceWrapper = { children: ReactNode; @@ -28,6 +29,9 @@ export const InstanceWrapper: FC = observer((props) => { const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), { revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnReconnect: false, + errorRetryCount: 0, }); if (isSWRLoading || isLoading) @@ -37,6 +41,15 @@ export const InstanceWrapper: FC = observer((props) => { ); + if (!instance) { + return ( + + ); + } + if (instance?.instance?.is_setup_done === false && authEnabled === "1") return ( diff --git a/admin/package.json b/admin/package.json index 6a63ea9372..936c612bb4 100644 --- a/admin/package.json +++ b/admin/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "turbo run develop", - "develop": "next dev --port 3333", + "develop": "next dev --port 3001", "build": "next build", "preview": "next build && next start", "start": "next start", diff --git a/admin/services/instance.service.ts b/admin/services/instance.service.ts index 519adc9f29..109b52e446 100644 --- a/admin/services/instance.service.ts +++ b/admin/services/instance.service.ts @@ -1,8 +1,8 @@ -import { APIService } from "services/api.service"; // types import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; +import { API_BASE_URL } from "@/helpers/common.helper"; +import { APIService } from "@/services/api.service"; export class InstanceService extends APIService { constructor() { diff --git a/apiserver/plane/authentication/adapter/error.py b/apiserver/plane/authentication/adapter/error.py index 4b975939d5..73809b9ad5 100644 --- a/apiserver/plane/authentication/adapter/error.py +++ b/apiserver/plane/authentication/adapter/error.py @@ -1,51 +1,52 @@ AUTHENTICATION_ERROR_CODES = { # Global "INSTANCE_NOT_CONFIGURED": 5000, - "INVALID_EMAIL": 5012, - "EMAIL_REQUIRED": 5013, - "SIGNUP_DISABLED": 5001, + "INVALID_EMAIL": 5005, + "EMAIL_REQUIRED": 5010, + "SIGNUP_DISABLED": 5015, # Password strength - "INVALID_PASSWORD": 5002, - "SMTP_NOT_CONFIGURED": 5007, + "INVALID_PASSWORD": 5020, + "SMTP_NOT_CONFIGURED": 5025, # Sign Up - "USER_ALREADY_EXIST": 5003, - "AUTHENTICATION_FAILED_SIGN_UP": 5006, - "REQUIRED_EMAIL_PASSWORD_SIGN_UP": 5015, - "INVALID_EMAIL_SIGN_UP": 5017, - "INVALID_EMAIL_MAGIC_SIGN_UP": 5019, - "MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED": 5023, + "USER_ALREADY_EXIST": 5030, + "AUTHENTICATION_FAILED_SIGN_UP": 5035, + "REQUIRED_EMAIL_PASSWORD_SIGN_UP": 5040, + "INVALID_EMAIL_SIGN_UP": 5045, + "INVALID_EMAIL_MAGIC_SIGN_UP": 5050, + "MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED": 5055, # Sign In - "USER_DOES_NOT_EXIST": 5004, - "AUTHENTICATION_FAILED_SIGN_IN": 5005, - "REQUIRED_EMAIL_PASSWORD_SIGN_IN": 5014, - "INVALID_EMAIL_SIGN_IN": 5016, - "INVALID_EMAIL_MAGIC_SIGN_IN": 5018, - "MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED": 5022, - # Both Sign in and Sign up - "INVALID_MAGIC_CODE": 5008, - "EXPIRED_MAGIC_CODE": 5009, + "USER_DOES_NOT_EXIST": 5060, + "AUTHENTICATION_FAILED_SIGN_IN": 5065, + "REQUIRED_EMAIL_PASSWORD_SIGN_IN": 5070, + "INVALID_EMAIL_SIGN_IN": 5075, + "INVALID_EMAIL_MAGIC_SIGN_IN": 5080, + "MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED": 5085, + # Both Sign in and Sign up for magic + "INVALID_MAGIC_CODE": 5090, + "EXPIRED_MAGIC_CODE": 5095, + "EMAIL_CODE_ATTEMPT_EXHAUSTED": 5100, # Oauth - "GOOGLE_NOT_CONFIGURED": 5010, - "GITHUB_NOT_CONFIGURED": 5011, - "GOOGLE_OAUTH_PROVIDER_ERROR": 5021, - "GITHUB_OAUTH_PROVIDER_ERROR": 5020, + "GOOGLE_NOT_CONFIGURED": 5105, + "GITHUB_NOT_CONFIGURED": 5110, + "GOOGLE_OAUTH_PROVIDER_ERROR": 5115, + "GITHUB_OAUTH_PROVIDER_ERROR": 5120, # Reset Password - "INVALID_PASSWORD_TOKEN": 5024, - "EXPIRED_PASSWORD_TOKEN": 5025, + "INVALID_PASSWORD_TOKEN": 5125, + "EXPIRED_PASSWORD_TOKEN": 5130, # Change password - "INCORRECT_OLD_PASSWORD": 5026, - "INVALID_NEW_PASSWORD": 5027, + "INCORRECT_OLD_PASSWORD": 5135, + "INVALID_NEW_PASSWORD": 5140, # set passowrd - "PASSWORD_ALREADY_SET": 5028, + "PASSWORD_ALREADY_SET": 5145, # Admin - "ADMIN_ALREADY_EXIST": 5029, - "REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME": 5030, - "INVALID_ADMIN_EMAIL": 5031, - "INVALID_ADMIN_PASSWORD": 5032, - "REQUIRED_ADMIN_EMAIL_PASSWORD": 5033, - "ADMIN_AUTHENTICATION_FAILED": 5034, - "ADMIN_USER_ALREADY_EXIST": 5035, - "ADMIN_USER_DOES_NOT_EXIST": 5036, + "ADMIN_ALREADY_EXIST": 5150, + "REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME": 5155, + "INVALID_ADMIN_EMAIL": 5160, + "INVALID_ADMIN_PASSWORD": 5165, + "REQUIRED_ADMIN_EMAIL_PASSWORD": 5170, + "ADMIN_AUTHENTICATION_FAILED": 5175, + "ADMIN_USER_ALREADY_EXIST": 5180, + "ADMIN_USER_DOES_NOT_EXIST": 5185, } diff --git a/apiserver/plane/authentication/provider/credentials/magic_code.py b/apiserver/plane/authentication/provider/credentials/magic_code.py index 71451ef0d6..c1207d14d5 100644 --- a/apiserver/plane/authentication/provider/credentials/magic_code.py +++ b/apiserver/plane/authentication/provider/credentials/magic_code.py @@ -77,7 +77,13 @@ class MagicCodeProvider(CredentialAdapter): current_attempt = data["current_attempt"] + 1 if data["current_attempt"] > 2: - return key, "" + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES[ + "EMAIL_CODE_ATTEMPT_EXHAUSTED" + ], + error_message="EMAIL_CODE_ATTEMPT_EXHAUSTED", + payload={"email": self.key}, + ) value = { "current_attempt": current_attempt, diff --git a/apiserver/plane/authentication/utils/host.py b/apiserver/plane/authentication/utils/host.py index b670eed41c..4046c1e207 100644 --- a/apiserver/plane/authentication/utils/host.py +++ b/apiserver/plane/authentication/utils/host.py @@ -5,21 +5,38 @@ from urllib.parse import urlsplit from django.conf import settings -def base_host(request, is_admin=False, is_space=False): +def base_host(request, is_admin=False, is_space=False, is_app=False): """Utility function to return host / origin from the request""" - - if is_admin and settings.ADMIN_BASE_URL: - return settings.ADMIN_BASE_URL - - if is_space and settings.SPACE_BASE_URL: - return settings.SPACE_BASE_URL - - return ( + # Calculate the base origin from request + base_origin = str( request.META.get("HTTP_ORIGIN") or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}" or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}""" ) + # Admin redirections + if is_admin: + if settings.ADMIN_BASE_URL: + return settings.ADMIN_BASE_URL + else: + return base_origin + "/god-mode/" + + # Space redirections + if is_space: + if settings.SPACE_BASE_URL: + return settings.SPACE_BASE_URL + else: + return base_origin + "/spaces/" + + # App Redirection + if is_app: + if settings.APP_BASE_URL: + return settings.APP_BASE_URL + else: + return base_origin + + return base_origin + def user_ip(request): return str(request.META.get("REMOTE_ADDR")) diff --git a/apiserver/plane/authentication/utils/login.py b/apiserver/plane/authentication/utils/login.py index 88a988c8f5..45dbdc2494 100644 --- a/apiserver/plane/authentication/utils/login.py +++ b/apiserver/plane/authentication/utils/login.py @@ -5,12 +5,17 @@ from django.contrib.auth import login from plane.authentication.utils.host import base_host -def user_login(request, user): +def user_login(request, user, is_app=False, is_admin=False, is_space=False): login(request=request, user=user) device_info = { "user_agent": request.META.get("HTTP_USER_AGENT", ""), "ip_address": request.META.get("REMOTE_ADDR", ""), - "domain": base_host(request=request), + "domain": base_host( + request=request, + is_app=is_app, + is_admin=is_admin, + is_space=is_space, + ), } request.session["device_info"] = device_info request.session.save() diff --git a/apiserver/plane/authentication/views/app/email.py b/apiserver/plane/authentication/views/app/email.py index 7ef6ac9f4c..4093be1080 100644 --- a/apiserver/plane/authentication/views/app/email.py +++ b/apiserver/plane/authentication/views/app/email.py @@ -42,8 +42,8 @@ class SignInAuthEndpoint(View): params["next_path"] = str(next_path) # Base URL join url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode(params), + base_host(request=request, is_app=True), + "sign-in?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -66,8 +66,8 @@ class SignInAuthEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode(params), + base_host(request=request, is_app=True), + "sign-in?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -85,8 +85,8 @@ class SignInAuthEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode(params), + base_host(request=request, is_app=True), + "sign-in?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -100,8 +100,8 @@ class SignInAuthEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode(params), + base_host(request=request, is_app=True), + "sign-in?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -111,7 +111,7 @@ class SignInAuthEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_app=True) # Process workspace and project invitations process_workspace_project_invitations(user=user) # Get the redirection path @@ -121,15 +121,15 @@ class SignInAuthEndpoint(View): path = get_redirection_path(user=user) # redirect to referer path - url = urljoin(base_host(request=request), path) + url = urljoin(base_host(request=request, is_app=True), path) return HttpResponseRedirect(url) except AuthenticationException as e: params = e.get_error_dict() if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode(params), + base_host(request=request, is_app=True), + "sign-in?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -152,7 +152,7 @@ class SignUpAuthEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -173,7 +173,7 @@ class SignUpAuthEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -192,7 +192,7 @@ class SignUpAuthEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -207,7 +207,7 @@ class SignUpAuthEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -218,7 +218,7 @@ class SignUpAuthEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_app=True) # Process workspace and project invitations process_workspace_project_invitations(user=user) # Get the redirection path @@ -227,14 +227,14 @@ class SignUpAuthEndpoint(View): else: path = get_redirection_path(user=user) # redirect to referer path - url = urljoin(base_host(request=request), path) + url = urljoin(base_host(request=request, is_app=True), path) return HttpResponseRedirect(url) except AuthenticationException as e: params = e.get_error_dict() if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) diff --git a/apiserver/plane/authentication/views/app/github.py b/apiserver/plane/authentication/views/app/github.py index 48b7e09d96..73afa674b9 100644 --- a/apiserver/plane/authentication/views/app/github.py +++ b/apiserver/plane/authentication/views/app/github.py @@ -24,7 +24,7 @@ class GitHubOauthInitiateEndpoint(View): def get(self, request): # Get host and next path - request.session["host"] = base_host(request=request) + request.session["host"] = base_host(request=request, is_app=True) next_path = request.GET.get("next_path") if next_path: request.session["next_path"] = str(next_path) @@ -42,7 +42,7 @@ class GitHubOauthInitiateEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -57,7 +57,7 @@ class GitHubOauthInitiateEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -110,7 +110,7 @@ class GitHubCallbackEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_app=True) # Process workspace and project invitations process_workspace_project_invitations(user=user) # Get the redirection path diff --git a/apiserver/plane/authentication/views/app/google.py b/apiserver/plane/authentication/views/app/google.py index 690a9778bd..ea3afed896 100644 --- a/apiserver/plane/authentication/views/app/google.py +++ b/apiserver/plane/authentication/views/app/google.py @@ -24,7 +24,7 @@ from plane.authentication.adapter.error import ( class GoogleOauthInitiateEndpoint(View): def get(self, request): - request.session["host"] = base_host(request=request) + request.session["host"] = base_host(request=request, is_app=True) next_path = request.GET.get("next_path") if next_path: request.session["next_path"] = str(next_path) @@ -42,7 +42,7 @@ class GoogleOauthInitiateEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -58,7 +58,7 @@ class GoogleOauthInitiateEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -108,7 +108,7 @@ class GoogleCallbackEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_app=True) # Process workspace and project invitations process_workspace_project_invitations(user=user) # Get the redirection path diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py index 8f9f3633b4..0fa5296740 100644 --- a/apiserver/plane/authentication/views/app/magic.py +++ b/apiserver/plane/authentication/views/app/magic.py @@ -90,8 +90,8 @@ class MagicSignInEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode(params), + base_host(request=request, is_app=True), + "sign-in?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -104,8 +104,8 @@ class MagicSignInEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode(params), + base_host(request=request, is_app=True), + "sign-in?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -116,7 +116,7 @@ class MagicSignInEndpoint(View): user = provider.authenticate() profile = Profile.objects.get(user=user) # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_app=True) # Process workspace and project invitations process_workspace_project_invitations(user=user) if user.is_password_autoset and profile.is_onboarded: @@ -129,7 +129,7 @@ class MagicSignInEndpoint(View): else str(process_workspace_project_invitations(user=user)) ) # redirect to referer path - url = urljoin(base_host(request=request), path) + url = urljoin(base_host(request=request, is_app=True), path) return HttpResponseRedirect(url) except AuthenticationException as e: @@ -137,8 +137,8 @@ class MagicSignInEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode(params), + base_host(request=request, is_app=True), + "sign-in?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -163,7 +163,7 @@ class MagicSignUpEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -177,7 +177,7 @@ class MagicSignUpEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -188,7 +188,7 @@ class MagicSignUpEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_app=True) # Process workspace and project invitations process_workspace_project_invitations(user=user) # Get the redirection path @@ -197,7 +197,7 @@ class MagicSignUpEndpoint(View): else: path = get_redirection_path(user=user) # redirect to referer path - url = urljoin(base_host(request=request), path) + url = urljoin(base_host(request=request, is_app=True), path) return HttpResponseRedirect(url) except AuthenticationException as e: @@ -205,7 +205,7 @@ class MagicSignUpEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) diff --git a/apiserver/plane/authentication/views/app/password_management.py b/apiserver/plane/authentication/views/app/password_management.py index 80803cd253..b26b577606 100644 --- a/apiserver/plane/authentication/views/app/password_management.py +++ b/apiserver/plane/authentication/views/app/password_management.py @@ -146,7 +146,7 @@ class ResetPasswordEndpoint(View): ) params = exc.get_error_dict() url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "accounts/reset-password?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -159,8 +159,9 @@ class ResetPasswordEndpoint(View): error_message="INVALID_PASSWORD", ) url = urljoin( - base_host(request=request), - "?" + urlencode(exc.get_error_dict()), + base_host(request=request, is_app=True), + "accounts/reset-password?" + + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -172,7 +173,7 @@ class ResetPasswordEndpoint(View): error_message="INVALID_PASSWORD", ) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "accounts/reset-password?" + urlencode(exc.get_error_dict()), ) @@ -184,8 +185,8 @@ class ResetPasswordEndpoint(View): user.save() url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode({"success": True}), + base_host(request=request, is_app=True), + "sign-in?" + urlencode({"success": True}), ) return HttpResponseRedirect(url) except DjangoUnicodeDecodeError: @@ -196,7 +197,7 @@ class ResetPasswordEndpoint(View): error_message="EXPIRED_PASSWORD_TOKEN", ) url = urljoin( - base_host(request=request), + base_host(request=request, is_app=True), "accounts/reset-password?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) diff --git a/apiserver/plane/authentication/views/app/signout.py b/apiserver/plane/authentication/views/app/signout.py index 967f398eb1..10461f240d 100644 --- a/apiserver/plane/authentication/views/app/signout.py +++ b/apiserver/plane/authentication/views/app/signout.py @@ -1,5 +1,5 @@ # Python imports -from urllib.parse import urlencode, urljoin +from urllib.parse import urljoin # Django imports from django.views import View @@ -23,12 +23,9 @@ class SignOutAuthEndpoint(View): user.save() # Log the user out logout(request) - url = urljoin( - base_host(request=request), - "accounts/sign-in?" + urlencode({"success": "true"}), - ) + url = urljoin(base_host(request=request, is_app=True), "sign-in") return HttpResponseRedirect(url) except Exception: return HttpResponseRedirect( - base_host(request=request), "accounts/sign-in" + base_host(request=request, is_app=True), "sign-in" ) diff --git a/apiserver/plane/authentication/views/common.py b/apiserver/plane/authentication/views/common.py index 4b93010de5..16ac058b02 100644 --- a/apiserver/plane/authentication/views/common.py +++ b/apiserver/plane/authentication/views/common.py @@ -70,7 +70,7 @@ class ChangePasswordEndpoint(APIView): user.set_password(serializer.data.get("new_password")) user.is_password_autoset = False user.save() - user_login(user=user, request=request) + user_login(user=user, request=request, is_app=True) return Response( {"message": "Password updated successfully"}, status=status.HTTP_200_OK, @@ -131,7 +131,7 @@ class SetUserPasswordEndpoint(APIView): user.is_password_autoset = False user.save() # Login the user as the session is invalidated - user_login(user=user, request=request) + user_login(user=user, request=request, is_app=True) # Return the user serializer = UserSerializer(user) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/authentication/views/space/email.py b/apiserver/plane/authentication/views/space/email.py index 4505332ebe..e11ab29b51 100644 --- a/apiserver/plane/authentication/views/space/email.py +++ b/apiserver/plane/authentication/views/space/email.py @@ -38,7 +38,7 @@ class SignInAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -60,7 +60,7 @@ class SignInAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces/accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -79,7 +79,7 @@ class SignInAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces/accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -94,7 +94,7 @@ class SignInAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces/accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -104,11 +104,11 @@ class SignInAuthSpaceEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_space=True) # redirect to next path url = urljoin( base_host(request=request, is_space=True), - str(next_path) if next_path else "/", + str(next_path) if next_path else "", ) return HttpResponseRedirect(url) except AuthenticationException as e: @@ -117,7 +117,7 @@ class SignInAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces/accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -141,7 +141,7 @@ class SignUpAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -162,7 +162,7 @@ class SignUpAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) # Validate the email @@ -181,7 +181,7 @@ class SignUpAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -196,7 +196,7 @@ class SignUpAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -206,11 +206,11 @@ class SignUpAuthSpaceEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_space=True) # redirect to referer path url = urljoin( base_host(request=request, is_space=True), - str(next_path) if next_path else "spaces", + str(next_path) if next_path else "", ) return HttpResponseRedirect(url) except AuthenticationException as e: @@ -219,6 +219,6 @@ class SignUpAuthSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) diff --git a/apiserver/plane/authentication/views/space/github.py b/apiserver/plane/authentication/views/space/github.py index 4a0f230984..8430cbdfb0 100644 --- a/apiserver/plane/authentication/views/space/github.py +++ b/apiserver/plane/authentication/views/space/github.py @@ -55,7 +55,7 @@ class GitHubOauthInitiateSpaceEndpoint(View): if next_path: params["next_path"] = str(next_path) url = urljoin( - base_host(request=request), + base_host(request=request, is_space=True), "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -108,10 +108,10 @@ class GitHubCallbackSpaceEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_space=True) # Process workspace and project invitations # redirect to referer path - url = urljoin(base_host, str(next_path) if next_path else "/") + url = urljoin(base_host, str(next_path) if next_path else "") return HttpResponseRedirect(url) except AuthenticationException as e: params = e.get_error_dict() diff --git a/apiserver/plane/authentication/views/space/google.py b/apiserver/plane/authentication/views/space/google.py index 2f6b576993..502f146c31 100644 --- a/apiserver/plane/authentication/views/space/google.py +++ b/apiserver/plane/authentication/views/space/google.py @@ -103,7 +103,7 @@ class GoogleCallbackSpaceEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_space=True) # redirect to referer path url = urljoin( base_host, str(next_path) if next_path else "/spaces" diff --git a/apiserver/plane/authentication/views/space/magic.py b/apiserver/plane/authentication/views/space/magic.py index 52771f71b9..45a8e37550 100644 --- a/apiserver/plane/authentication/views/space/magic.py +++ b/apiserver/plane/authentication/views/space/magic.py @@ -86,7 +86,7 @@ class MagicSignInSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces/accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -99,7 +99,7 @@ class MagicSignInSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -109,14 +109,14 @@ class MagicSignInSpaceEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_space=True) # redirect to referer path profile = Profile.objects.get(user=user) if user.is_password_autoset and profile.is_onboarded: - path = "spaces/accounts/set-password" + path = "accounts/set-password" else: # Get the redirection path - path = str(next_path) if next_path else "spaces" + path = str(next_path) if next_path else "" url = urljoin(base_host(request=request, is_space=True), path) return HttpResponseRedirect(url) @@ -126,7 +126,7 @@ class MagicSignInSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces/accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -152,7 +152,7 @@ class MagicSignUpSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces/accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) @@ -176,7 +176,7 @@ class MagicSignUpSpaceEndpoint(View): ) user = provider.authenticate() # Login the user and record his device info - user_login(request=request, user=user) + user_login(request=request, user=user, is_space=True) # redirect to referer path url = urljoin( base_host(request=request, is_space=True), @@ -190,6 +190,6 @@ class MagicSignUpSpaceEndpoint(View): params["next_path"] = str(next_path) url = urljoin( base_host(request=request, is_space=True), - "spaces/accounts/sign-in?" + urlencode(params), + "?" + urlencode(params), ) return HttpResponseRedirect(url) diff --git a/apiserver/plane/authentication/views/space/password_management.py b/apiserver/plane/authentication/views/space/password_management.py index aeac9776d1..5263c89566 100644 --- a/apiserver/plane/authentication/views/space/password_management.py +++ b/apiserver/plane/authentication/views/space/password_management.py @@ -183,11 +183,9 @@ class ResetPasswordSpaceEndpoint(View): user.is_password_autoset = False user.save() - url = urljoin( - base_host(request=request, is_space=True), - "accounts/sign-in?" + urlencode({"success": True}), + return HttpResponseRedirect( + base_host(request=request, is_space=True) ) - return HttpResponseRedirect(url) except DjangoUnicodeDecodeError: exc = AuthenticationException( error_code=AUTHENTICATION_ERROR_CODES[ diff --git a/apiserver/plane/authentication/views/space/signout.py b/apiserver/plane/authentication/views/space/signout.py index 3cfd6d4711..655d8b1c8c 100644 --- a/apiserver/plane/authentication/views/space/signout.py +++ b/apiserver/plane/authentication/views/space/signout.py @@ -23,12 +23,10 @@ class SignOutAuthSpaceEndpoint(View): user.save() # Log the user out logout(request) - url = urljoin( - base_host(request=request, is_space=True), - "accounts/sign-in?" + urlencode({"success": "true"}), + return HttpResponseRedirect( + base_host(request=request, is_space=True) ) - return HttpResponseRedirect(url) except Exception: return HttpResponseRedirect( - base_host(request=request, is_space=True), "accounts/sign-in" + base_host(request=request, is_space=True) ) diff --git a/apiserver/plane/license/api/views/admin.py b/apiserver/plane/license/api/views/admin.py index ed3c00f17a..945f4b1b17 100644 --- a/apiserver/plane/license/api/views/admin.py +++ b/apiserver/plane/license/api/views/admin.py @@ -107,7 +107,7 @@ class InstanceAdminSignUpEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/setup?" + urlencode(exc.get_error_dict()), + "setup?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -119,7 +119,7 @@ class InstanceAdminSignUpEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/setup?" + urlencode(exc.get_error_dict()), + "setup?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -148,7 +148,7 @@ class InstanceAdminSignUpEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/setup?" + urlencode(exc.get_error_dict()), + "setup?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -170,7 +170,7 @@ class InstanceAdminSignUpEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/setup?" + urlencode(exc.get_error_dict()), + "setup?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -192,7 +192,7 @@ class InstanceAdminSignUpEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/setup?" + urlencode(exc.get_error_dict()), + "setup?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) else: @@ -214,7 +214,7 @@ class InstanceAdminSignUpEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/setup?" + urlencode(exc.get_error_dict()), + "setup?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -247,10 +247,8 @@ class InstanceAdminSignUpEndpoint(View): instance.save() # get tokens for user - user_login(request=request, user=user) - url = urljoin( - base_host(request=request, is_admin=True), "god-mode/general" - ) + user_login(request=request, user=user, is_admin=True) + url = urljoin(base_host(request=request, is_admin=True), "general") return HttpResponseRedirect(url) @@ -272,7 +270,7 @@ class InstanceAdminSignInEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/login?" + urlencode(exc.get_error_dict()), + "?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -293,7 +291,7 @@ class InstanceAdminSignInEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/login?" + urlencode(exc.get_error_dict()), + "?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -311,7 +309,7 @@ class InstanceAdminSignInEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/login?" + urlencode(exc.get_error_dict()), + "?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -331,7 +329,7 @@ class InstanceAdminSignInEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/login?" + urlencode(exc.get_error_dict()), + "?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -348,7 +346,7 @@ class InstanceAdminSignInEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/login?" + urlencode(exc.get_error_dict()), + "?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) @@ -365,7 +363,7 @@ class InstanceAdminSignInEndpoint(View): ) url = urljoin( base_host(request=request, is_admin=True), - "god-mode/login?" + urlencode(exc.get_error_dict()), + "?" + urlencode(exc.get_error_dict()), ) return HttpResponseRedirect(url) # settings last active for the user @@ -378,10 +376,8 @@ class InstanceAdminSignInEndpoint(View): user.save() # get tokens for user - user_login(request=request, user=user) - url = urljoin( - base_host(request=request, is_admin=True), "god-mode/general" - ) + user_login(request=request, user=user, is_admin=True) + url = urljoin(base_host(request=request, is_admin=True), "general") return HttpResponseRedirect(url) @@ -414,12 +410,9 @@ class InstanceAdminSignOutEndpoint(View): user.save() # Log the user out logout(request) - url = urljoin( - base_host(request=request, is_admin=True), - "accounts/sign-in?" + urlencode({"success": "true"}), - ) + url = urljoin(base_host(request=request, is_admin=True)) return HttpResponseRedirect(url) except Exception: return HttpResponseRedirect( - base_host(request=request, is_admin=True), "accounts/sign-in" + base_host(request=request, is_admin=True) ) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index faff39e165..dbdb890ea8 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -349,4 +349,4 @@ CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None) # Base URLs ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None) SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None) -APP_BASE_URL = os.environ.get("ADMIN_BASE_URL", None) +APP_BASE_URL = os.environ.get("APP_BASE_URL") or os.environ.get("WEB_URL") diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 2290262aef..b96d1ca31b 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -35,10 +35,10 @@ CORS_ALLOWED_ORIGINS = [ "http://127.0.0.1", "http://localhost:3000", "http://127.0.0.1:3000", - "http://localhost:4000", - "http://127.0.0.1:4000", - "http://localhost:3333", - "http://127.0.0.1:3333", + "http://localhost:3001", + "http://127.0.0.1:3001", + "http://localhost:3002", + "http://127.0.0.1:3002", ] CSRF_TRUSTED_ORIGINS = CORS_ALLOWED_ORIGINS CORS_ALLOW_ALL_ORIGINS = True diff --git a/apiserver/runtime.txt b/apiserver/runtime.txt index cd0aac5427..8cf46af5fc 100644 --- a/apiserver/runtime.txt +++ b/apiserver/runtime.txt @@ -1 +1 @@ -python-3.11.9 \ No newline at end of file +python-3.12.3 \ No newline at end of file diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index 67f61d0ef4..6f58113bee 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -42,6 +42,7 @@ services: web: <<: *app-env image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable} + platform: ${DOCKER_PLATFORM:-} pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: node web/server.js web @@ -54,6 +55,7 @@ services: space: <<: *app-env image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable} + platform: ${DOCKER_PLATFORM:-} pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: node space/server.js space @@ -66,7 +68,8 @@ services: admin: <<: *app-env - image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable} + image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable} + platform: ${DOCKER_PLATFORM:-} pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: node admin/server.js admin @@ -79,6 +82,7 @@ services: api: <<: *app-env image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable} + platform: ${DOCKER_PLATFORM:-} pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: ./bin/takeoff @@ -93,6 +97,7 @@ services: worker: <<: *app-env image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable} + platform: ${DOCKER_PLATFORM:-} pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: ./bin/worker @@ -106,6 +111,7 @@ services: beat-worker: <<: *app-env image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable} + platform: ${DOCKER_PLATFORM:-} pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: ./bin/beat @@ -119,6 +125,7 @@ services: migrator: <<: *app-env image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable} + platform: ${DOCKER_PLATFORM:-} pull_policy: ${PULL_POLICY:-always} restart: no command: > @@ -138,6 +145,7 @@ services: command: postgres -c 'max_connections=1000' volumes: - pgdata:/var/lib/postgresql/data + plane-redis: <<: *app-env image: redis:7.2.4-alpine @@ -148,7 +156,7 @@ services: plane-minio: <<: *app-env - image: minio/minio + image: minio/minio:latest pull_policy: if_not_present restart: unless-stopped command: server /export --console-address ":9090" @@ -159,6 +167,7 @@ services: proxy: <<: *app-env image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable} + platform: ${DOCKER_PLATFORM:-} pull_policy: ${PULL_POLICY:-always} ports: - ${NGINX_PORT}:80 diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index aaf1295241..b36d3b6b28 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -2,7 +2,8 @@ BRANCH=master SCRIPT_DIR=$PWD -PLANE_INSTALL_DIR=$PWD/plane-app +SERVICE_FOLDER=plane-app +PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER export APP_RELEASE=$BRANCH export DOCKERHUB_USER=makeplane export PULL_POLICY=always @@ -140,7 +141,7 @@ function download() { function startServices() { /bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --quiet-pull" - local migrator_container_id=$(docker container ls -aq -f "name=plane-app-migrator") + local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator") if [ -n "$migrator_container_id" ]; then local idx=0 while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do @@ -168,7 +169,7 @@ function startServices() { fi fi - local api_container_id=$(docker container ls -q -f "name=plane-app-api") + local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api") local idx2=0 while ! docker logs $api_container_id 2>&1 | grep -m 1 -i "Application startup complete" | grep -q "."; do @@ -408,7 +409,8 @@ fi # REMOVE SPECIAL CHARACTERS FROM BRANCH NAME if [ "$BRANCH" != "master" ]; then - PLANE_INSTALL_DIR=$PWD/plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g') + SERVICE_FOLDER=plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g') + PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER fi mkdir -p $PLANE_INSTALL_DIR/archive diff --git a/deploy/selfhost/variables.env b/deploy/selfhost/variables.env index 91e206bb49..62c4bc164a 100644 --- a/deploy/selfhost/variables.env +++ b/deploy/selfhost/variables.env @@ -43,3 +43,6 @@ FILE_SIZE_LIMIT=5242880 # Gunicorn Workers GUNICORN_WORKERS=1 + +# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE` +# DOCKER_PLATFORM=linux/amd64 \ No newline at end of file diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index ce2cf3ad6b..b82b1f3542 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -97,6 +97,9 @@ const replaceCodeBlockWithContent = (editor: Editor) => { const startPos = pos; const endPos = pos + node.nodeSize; const textContent = node.textContent; + if (textContent.length === 0) { + editor.chain().focus().toggleCodeBlock().run(); + } replaceCodeBlock(startPos, endPos, textContent); return false; } diff --git a/packages/editor/core/src/ui/extensions/table/table/table.ts b/packages/editor/core/src/ui/extensions/table/table/table.ts index 5fd06caf6a..c1f65feec3 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table.ts +++ b/packages/editor/core/src/ui/extensions/table/table/table.ts @@ -218,15 +218,21 @@ export const Table = Node.create({ addKeyboardShortcuts() { return { Tab: () => { - if (this.editor.commands.goToNextCell()) { - return true; - } + if (this.editor.isActive("table")) { + if (this.editor.isActive("listItem") || this.editor.isActive("taskItem")) { + return false; + } + if (this.editor.commands.goToNextCell()) { + return true; + } - if (!this.editor.can().addRowAfter()) { - return false; - } + if (!this.editor.can().addRowAfter()) { + return false; + } - return this.editor.chain().addRowAfter().goToNextCell().run(); + return this.editor.chain().addRowAfter().goToNextCell().run(); + } + return false; }, "Shift-Tab": () => this.editor.commands.goToPreviousCell(), Backspace: deleteTableWhenAllCellsSelected, diff --git a/packages/editor/extensions/src/extensions/drag-drop.tsx b/packages/editor/extensions/src/extensions/drag-drop.tsx index ab2df31adf..32867a5f17 100644 --- a/packages/editor/extensions/src/extensions/drag-drop.tsx +++ b/packages/editor/extensions/src/extensions/drag-drop.tsx @@ -14,6 +14,21 @@ export interface DragHandleOptions { }; } +export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) => + Extension.create({ + name: "dragAndDrop", + + addProseMirrorPlugins() { + return [ + DragHandle({ + dragHandleWidth: 24, + scrollThreshold: { up: 300, down: 100 }, + setHideDragHandle, + }), + ]; + }, + }); + function createDragHandleElement(): HTMLElement { const dragHandleElement = document.createElement("div"); dragHandleElement.draggable = true; @@ -49,23 +64,31 @@ function absoluteRect(node: Element) { } function nodeDOMAtCoords(coords: { x: number; y: number }) { - return document - .elementsFromPoint(coords.x, coords.y) - .find( - (elem: Element) => - elem.parentElement?.matches?.(".ProseMirror") || - elem.matches( - [ - "li", - "p:not(:first-child)", - ".code-block", - "blockquote", - "h1, h2, h3", - "table", - "[data-type=horizontalRule]", - ].join(", ") - ) - ); + const elements = document.elementsFromPoint(coords.x, coords.y); + const generalSelectors = [ + "li", + "p:not(:first-child)", + ".code-block", + "blockquote", + "h1, h2, h3", + ".table-wrapper", + "[data-type=horizontalRule]", + ].join(", "); + + for (const elem of elements) { + // if the element is a

tag that is the first child of a td or th + if ( + (elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) && + elem?.textContent?.trim() !== "" + ) { + return elem; // Return only if p tag is not empty + } + // apply general selector + if (elem.matches(generalSelectors)) { + return elem; + } + } + return null; } function nodePosAtDOM(node: Element, view: EditorView, options: DragHandleOptions) { @@ -86,15 +109,19 @@ function nodePosAtDOMForBlockquotes(node: Element, view: EditorView) { })?.inside; } -function calcNodePos(pos: number, view: EditorView) { +function calcNodePos(pos: number, view: EditorView, node: Element) { const maxPos = view.state.doc.content.size; const safePos = Math.max(0, Math.min(pos, maxPos)); const $pos = view.state.doc.resolve(safePos); if ($pos.depth > 1) { - const newPos = $pos.before($pos.depth); - return Math.max(0, Math.min(newPos, maxPos)); + if (node.matches("ul:not([data-type=taskList]) li, ol li")) { + // only for nested lists + const newPos = $pos.before($pos.depth); + return Math.max(0, Math.min(newPos, maxPos)); + } } + return safePos; } @@ -114,12 +141,12 @@ function DragHandle(options: DragHandleOptions) { let draggedNodePos = nodePosAtDOM(node, view, options); if (draggedNodePos == null || draggedNodePos < 0) return; - draggedNodePos = calcNodePos(draggedNodePos, view); + draggedNodePos = calcNodePos(draggedNodePos, view, node); const { from, to } = view.state.selection; const diff = from - to; - const fromSelectionPos = calcNodePos(from, view); + const fromSelectionPos = calcNodePos(from, view, node); let differentNodeSelected = false; const nodePos = view.state.doc.resolve(fromSelectionPos); @@ -148,6 +175,19 @@ function DragHandle(options: DragHandleOptions) { listType = node.parentElement!.tagName; } + if (node.matches("blockquote")) { + let nodePosForBlockquotes = nodePosAtDOMForBlockquotes(node, view); + if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return; + + const docSize = view.state.doc.content.size; + nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize)); + + if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) { + const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes); + view.dispatch(view.state.tr.setSelection(nodeSelection)); + } + } + const slice = view.state.selection.content(); const { dom, text } = __serializeForClipboard(view, slice); @@ -190,7 +230,7 @@ function DragHandle(options: DragHandleOptions) { if (nodePos === null || nodePos === undefined) return; // Adjust the nodePos to point to the start of the node, ensuring NodeSelection can be applied - nodePos = calcNodePos(nodePos, view); + nodePos = calcNodePos(nodePos, view, node); // Use NodeSelection to select the node at the calculated position const nodeSelection = NodeSelection.create(view.state.doc, nodePos); @@ -279,9 +319,11 @@ function DragHandle(options: DragHandleOptions) { // Li markers if (node.matches("ul:not([data-type=taskList]) li, ol li")) { - rect.top += 4; rect.left -= 18; } + if (node.matches(".table-wrapper")) { + rect.top += 8; + } rect.width = options.dragHandleWidth; @@ -352,18 +394,3 @@ function DragHandle(options: DragHandleOptions) { }, }); } - -export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) => - Extension.create({ - name: "dragAndDrop", - - addProseMirrorPlugins() { - return [ - DragHandle({ - dragHandleWidth: 24, - scrollThreshold: { up: 300, down: 100 }, - setHideDragHandle, - }), - ]; - }, - }); diff --git a/packages/editor/extensions/src/extensions/slash-commands.tsx b/packages/editor/extensions/src/extensions/slash-commands.tsx index 752fbe63cb..c1b1ef9c04 100644 --- a/packages/editor/extensions/src/extensions/slash-commands.tsx +++ b/packages/editor/extensions/src/extensions/slash-commands.tsx @@ -315,7 +315,10 @@ const CommandList = ({ items, command }: { items: CommandItemProps[]; command: a "bg-custom-background-80": index === selectedIndex, } )} - onClick={() => selectItem(index)} + onClick={(e) => { + e.stopPropagation(); + selectItem(index); + }} > {item.icon}

{item.title}

diff --git a/space/components/accounts/onboarding-form.tsx b/space/components/accounts/onboarding-form.tsx index 7a719d9385..768d5160fc 100644 --- a/space/components/accounts/onboarding-form.tsx +++ b/space/components/accounts/onboarding-form.tsx @@ -140,8 +140,11 @@ export const OnBoardingForm: React.FC = observer((props) => {
-
-