mirror of
https://github.com/makeplane/plane.git
synced 2026-02-23 19:50:27 +01:00
Merge pull request #234 from makeplane/sync/ce-ee
sync: merge conflicts need to be resolved
This commit is contained in:
@@ -4,7 +4,7 @@ module.exports = {
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/"],
|
||||
rootDir: ["web/", "space/", "admin/"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{SITE_TITLE}</title>
|
||||
<meta property="og:site_name" content={SITE_NAME} />
|
||||
<meta property="og:title" content={SITE_TITLE} />
|
||||
<meta property="og:url" content={SITE_URL} />
|
||||
<meta name="description" content={SITE_DESCRIPTION} />
|
||||
<meta property="og:description" content={SITE_DESCRIPTION} />
|
||||
<meta name="keywords" content={SITE_KEYWORDS} />
|
||||
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${prefix}favicon/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${prefix}favicon/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${prefix}favicon/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${prefix}site.webmanifest.json`} />
|
||||
<link rel="shortcut icon" href={`${prefix}favicon/favicon.ico`} />
|
||||
</head>
|
||||
<body className={`antialiased`}>
|
||||
<StoreProvider {...pageProps}>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<AppWrapper>{children}</AppWrapper>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{SITE_TITLE}</title>
|
||||
<meta property="og:site_name" content={SITE_NAME} />
|
||||
<meta property="og:title" content={SITE_TITLE} />
|
||||
<meta property="og:url" content={SITE_URL} />
|
||||
<meta name="description" content={SITE_DESCRIPTION} />
|
||||
<meta property="og:description" content={SITE_DESCRIPTION} />
|
||||
<meta name="keywords" content={SITE_KEYWORDS} />
|
||||
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${ASSET_PREFIX}/favicon/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${ASSET_PREFIX}/favicon/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${ASSET_PREFIX}/favicon/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${ASSET_PREFIX}/site.webmanifest.json`} />
|
||||
<link rel="shortcut icon" href={`${ASSET_PREFIX}/favicon/favicon.ico`} />
|
||||
</head>
|
||||
<body className={`antialiased`}>
|
||||
<StoreProvider {...pageProps}>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<AppWrapper>{children}</AppWrapper>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
export default RootLayout;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
46
admin/components/common/empty-state.tsx
Normal file
46
admin/components/common/empty-state.tsx
Normal file
@@ -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<Props> = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
disabled = false,
|
||||
}) => (
|
||||
<div className={`flex h-full w-full items-center justify-center`}>
|
||||
<div className="flex w-full flex-col items-center text-center">
|
||||
{image && <Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
|
||||
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -4,3 +4,4 @@ export * from "./controller-input";
|
||||
export * from "./copy-field";
|
||||
export * from "./password-strength-meter";
|
||||
export * from "./banner";
|
||||
export * from "./empty-state";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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";
|
||||
3
admin/hooks/store/index.ts
Normal file
3
admin/hooks/store/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./use-theme";
|
||||
export * from "./use-instance";
|
||||
export * from "./use-user";
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<TInstanceWrapper> = 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<TInstanceWrapper> = observer((props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!instance) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="Your instance wasn't configured successfully."
|
||||
description="Please try re-installing Plane to fix the problem. If the issue still persists please reach out to support@plane.so."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
|
||||
return (
|
||||
<DefaultLayout withoutBackground>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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[
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
python-3.11.9
|
||||
python-3.12.3
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <p> 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,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
<span className="grid place-items-center flex-shrink-0">{item.icon}</span>
|
||||
<p className="flex-grow truncate">{item.title}</p>
|
||||
|
||||
@@ -140,8 +140,11 @@ export const OnBoardingForm: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="first_name">
|
||||
<div className="space-y-1 w-full">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="first_name"
|
||||
>
|
||||
First name
|
||||
</label>
|
||||
<Controller
|
||||
@@ -171,8 +174,11 @@ export const OnBoardingForm: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
{errors.first_name && <span className="text-sm text-red-500">{errors.first_name.message}</span>}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="last_name">
|
||||
<div className="space-y-1 w-full">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="last_name"
|
||||
>
|
||||
Last name
|
||||
</label>
|
||||
<Controller
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_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 = SPACE_BASE_PATH;
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
||||
@@ -6,5 +6,5 @@ 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.userProfile;
|
||||
};
|
||||
|
||||
@@ -32,13 +32,17 @@ export const AuthWrapper: FC<TAuthWrapper> = observer((props) => {
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (pageType === EPageTypes.PUBLIC) return <>{children}</>;
|
||||
|
||||
if (pageType === EPageTypes.INIT) {
|
||||
if (!currentUser?.id) return <>{children}</>;
|
||||
else {
|
||||
if (currentUserProfile?.id && currentUserProfile?.onboarding_step?.profile_complete) return <>{children}</>;
|
||||
if (
|
||||
currentUserProfile &&
|
||||
currentUserProfile?.id &&
|
||||
Boolean(currentUserProfile?.onboarding_step?.profile_complete)
|
||||
)
|
||||
return <>{children}</>;
|
||||
else {
|
||||
router.push(`/onboarding`);
|
||||
return <></>;
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -9,15 +9,14 @@ import { Avatar } from "@plane/ui";
|
||||
import { OnBoardingForm } from "@/components/accounts/onboarding-form";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
import { ASSET_PREFIX } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useUser, useUserProfile } from "@/hooks/store";
|
||||
// wrappers
|
||||
import { AuthWrapper } from "@/lib/wrappers";
|
||||
// assets
|
||||
import ProfileSetupDark from "public/onboarding/profile-setup-dark.svg";
|
||||
import ProfileSetup from "public/onboarding/profile-setup.svg";
|
||||
|
||||
const imagePrefix = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
|
||||
import ProfileSetup from "public/onboarding/profile-setup-light.svg";
|
||||
|
||||
const OnBoardingPage = observer(() => {
|
||||
// router
|
||||
@@ -48,8 +47,8 @@ const OnBoardingPage = observer(() => {
|
||||
console.log("Failed to update onboarding status");
|
||||
});
|
||||
|
||||
if (next_path) router.replace(next_path.toString());
|
||||
router.replace("/");
|
||||
if (next_path) router.push(next_path.toString());
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -60,7 +59,7 @@ const OnBoardingPage = observer(() => {
|
||||
<div className="flex w-full items-center justify-between font-semibold ">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Image
|
||||
src={`${imagePrefix}/plane-logos/blue-without-text.png`}
|
||||
src={`${ASSET_PREFIX}/plane-logos/blue-without-text.png`}
|
||||
height={30}
|
||||
width={30}
|
||||
alt="Plane Logo"
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 729 KiB After Width: | Height: | Size: 994 KiB |
407
space/public/onboarding/profile-setup-light.svg
Normal file
407
space/public/onboarding/profile-setup-light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 993 KiB |
@@ -4,7 +4,6 @@ import { enableStaticRendering } from "mobx-react-lite";
|
||||
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 IssueStore, { IIssueStore } from "./issue";
|
||||
import IssueDetailStore, { IIssueDetailStore } from "./issue_details";
|
||||
@@ -16,7 +15,6 @@ enableStaticRendering(typeof window === "undefined");
|
||||
export class RootStore {
|
||||
instance: IInstanceStore;
|
||||
user: IUserStore;
|
||||
profile: IProfileStore;
|
||||
project: IProjectStore;
|
||||
|
||||
issue: IIssueStore;
|
||||
@@ -27,9 +25,8 @@ export class RootStore {
|
||||
constructor() {
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.profile = new ProfileStore(this);
|
||||
this.project = new ProjectStore(this);
|
||||
|
||||
this.project = new ProjectStore(this);
|
||||
this.issue = new IssueStore(this);
|
||||
this.issueDetails = new IssueDetailStore(this);
|
||||
this.mentionsStore = new MentionsStore(this);
|
||||
@@ -41,7 +38,6 @@ export class RootStore {
|
||||
|
||||
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);
|
||||
|
||||
@@ -36,13 +36,13 @@ ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH=""
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH=""
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
@@ -71,13 +71,13 @@ ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH=""
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
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_SPACE_BASE_PATH=""
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
AuthHeader,
|
||||
@@ -43,6 +42,7 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
|
||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||
const [isPasswordAutoset, setIsPasswordAutoset] = useState(true);
|
||||
// hooks
|
||||
const { instance } = useInstance();
|
||||
|
||||
@@ -63,52 +63,57 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
|
||||
)
|
||||
)
|
||||
setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
|
||||
// validating weather to show alert to banner
|
||||
if (errorhandler?.type === EErrorAlertType.TOAST_ALERT) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: errorhandler?.title,
|
||||
message: errorhandler?.message as string,
|
||||
});
|
||||
} else setErrorInfo(errorhandler);
|
||||
setErrorInfo(errorhandler);
|
||||
}
|
||||
}
|
||||
}, [error_code, authMode]);
|
||||
|
||||
// step 1 submit handler- email verification
|
||||
// submit handler- email verification
|
||||
const handleEmailVerification = async (data: IEmailCheckData) => {
|
||||
setEmail(data.email);
|
||||
|
||||
const emailCheckRequest =
|
||||
authMode === EAuthModes.SIGN_IN ? authService.signInEmailCheck(data) : authService.signUpEmailCheck(data);
|
||||
|
||||
await emailCheckRequest
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
if (authMode === EAuthModes.SIGN_IN) {
|
||||
if (response.is_password_autoset) setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
else setAuthStep(EAuthSteps.PASSWORD);
|
||||
if (response.is_password_autoset) {
|
||||
setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
generateEmailUniqueCode(data.email);
|
||||
} else {
|
||||
setIsPasswordAutoset(false);
|
||||
setAuthStep(EAuthSteps.PASSWORD);
|
||||
}
|
||||
} else {
|
||||
if (instance && instance?.config?.is_smtp_configured) setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
else setAuthStep(EAuthSteps.PASSWORD);
|
||||
if (instance && instance?.config?.is_smtp_configured) {
|
||||
setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
generateEmailUniqueCode(data.email);
|
||||
} else setAuthStep(EAuthSteps.PASSWORD);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorhandler = authErrorHandler(error?.error_code.toString(), data?.email || undefined);
|
||||
if (errorhandler?.type === EErrorAlertType.BANNER_ALERT) {
|
||||
setErrorInfo(errorhandler);
|
||||
return;
|
||||
} else if (errorhandler?.type === EErrorAlertType.TOAST_ALERT)
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: errorhandler?.title,
|
||||
message: (errorhandler?.message as string) || "Something went wrong. Please try again.",
|
||||
});
|
||||
if (errorhandler?.type) setErrorInfo(errorhandler);
|
||||
});
|
||||
};
|
||||
|
||||
// generating the unique code
|
||||
const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => {
|
||||
const payload = { email: email };
|
||||
return await authService
|
||||
.generateUniqueCode(payload)
|
||||
.then(() => ({ code: "" }))
|
||||
.catch((error) => {
|
||||
const errorhandler = authErrorHandler(error?.error_code.toString());
|
||||
if (errorhandler?.type) setErrorInfo(errorhandler);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const isOAuthEnabled =
|
||||
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
|
||||
(instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled)) || false;
|
||||
|
||||
const isSMTPConfigured = (instance?.config && instance?.config?.is_smtp_configured) || false;
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col space-y-6">
|
||||
@@ -125,24 +130,29 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
|
||||
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
||||
{authStep === EAuthSteps.UNIQUE_CODE && (
|
||||
<AuthUniqueCodeForm
|
||||
mode={authMode}
|
||||
email={email}
|
||||
handleEmailClear={() => {
|
||||
setEmail("");
|
||||
setAuthStep(EAuthSteps.EMAIL);
|
||||
}}
|
||||
submitButtonText="Continue"
|
||||
mode={authMode}
|
||||
generateEmailUniqueCode={generateEmailUniqueCode}
|
||||
/>
|
||||
)}
|
||||
{authStep === EAuthSteps.PASSWORD && (
|
||||
<AuthPasswordForm
|
||||
mode={authMode}
|
||||
isPasswordAutoset={isPasswordAutoset}
|
||||
isSMTPConfigured={isSMTPConfigured}
|
||||
email={email}
|
||||
handleEmailClear={() => {
|
||||
setEmail("");
|
||||
setAuthStep(EAuthSteps.EMAIL);
|
||||
}}
|
||||
handleStepChange={(step) => setAuthStep(step)}
|
||||
mode={authMode}
|
||||
handleAuthStep={(step: EAuthSteps) => {
|
||||
if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email);
|
||||
setAuthStep(step);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOAuthEnabled && <OAuthOptions />}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IEmailCheckData } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||
|
||||
type TAuthEmailForm = {
|
||||
@@ -19,6 +20,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [email, setEmail] = useState(defaultEmail);
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
|
||||
const emailError = useMemo(
|
||||
() => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined),
|
||||
@@ -38,31 +40,36 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
||||
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit} className="mt-8 space-y-4">
|
||||
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<div
|
||||
className={cn(
|
||||
`relative flex items-center rounded-md bg-onboarding-background-200 border`,
|
||||
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100`
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
hasError={Boolean(emailError?.email)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
autoFocus
|
||||
/>
|
||||
{email.length > 0 && (
|
||||
<XCircle
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => setEmail("")}
|
||||
/>
|
||||
<div className="flex-shrink-0 h-5 w-5 mr-2 bg-onboarding-background-200 hover:cursor-pointer">
|
||||
<XCircle className="h-5 w-5 stroke-custom-text-400" onClick={() => setEmail("")} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{emailError?.email && (
|
||||
{emailError?.email && !isFocused && (
|
||||
<p className="flex items-center gap-1 text-xs text-red-600 px-0.5">
|
||||
<CircleAlert height={12} width={12} />
|
||||
{emailError.email}
|
||||
|
||||
@@ -14,15 +14,17 @@ import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { getPasswordStrength } from "@/helpers/password.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useInstance } from "@/hooks/store";
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
isPasswordAutoset: boolean;
|
||||
isSMTPConfigured: boolean;
|
||||
mode: EAuthModes;
|
||||
handleStepChange: (step: EAuthSteps) => void;
|
||||
handleEmailClear: () => void;
|
||||
handleAuthStep: (step: EAuthSteps) => void;
|
||||
};
|
||||
|
||||
type TPasswordFormValues = {
|
||||
@@ -39,9 +41,8 @@ const defaultValues: TPasswordFormValues = {
|
||||
const authService = new AuthService();
|
||||
|
||||
export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
const { email, handleStepChange, handleEmailClear, mode } = props;
|
||||
const { email, isSMTPConfigured, handleAuthStep, handleEmailClear, mode } = props;
|
||||
// hooks
|
||||
const { instance } = useInstance();
|
||||
const { captureEvent } = useEventTracker();
|
||||
// states
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
@@ -56,9 +57,6 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
const handleShowPassword = (key: keyof typeof showPassword) =>
|
||||
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
|
||||
// derived values
|
||||
const isSmtpConfigured = instance?.config?.is_smtp_configured;
|
||||
|
||||
const handleFormChange = (key: keyof TPasswordFormValues, value: string) =>
|
||||
setPasswordFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
@@ -68,13 +66,13 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
}, [csrfToken]);
|
||||
|
||||
const redirectToUniqueCodeSignIn = async () => {
|
||||
handleStepChange(EAuthSteps.UNIQUE_CODE);
|
||||
handleAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
};
|
||||
|
||||
const passwordSupport =
|
||||
mode === EAuthModes.SIGN_IN ? (
|
||||
<div className="mt-2 w-full pb-3">
|
||||
{isSmtpConfigured ? (
|
||||
<div className="w-full">
|
||||
{isSMTPConfigured ? (
|
||||
<Link
|
||||
onClick={() => captureEvent(FORGOT_PASSWORD)}
|
||||
href={`/accounts/forgot-password?email=${email}`}
|
||||
@@ -87,7 +85,10 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
isPasswordInputFocused && <PasswordStrengthMeter password={passwordFormData.password} />
|
||||
passwordFormData.password.length > 0 &&
|
||||
(getPasswordStrength(passwordFormData.password) < 3 || isPasswordInputFocused) && (
|
||||
<PasswordStrengthMeter password={passwordFormData.password} />
|
||||
)
|
||||
);
|
||||
|
||||
const isButtonDisabled = useMemo(
|
||||
@@ -112,11 +113,14 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
onError={() => setIsSubmitting(false)}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<input type="hidden" value={passwordFormData.email} name="email" />
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<div
|
||||
className={`relative flex items-center rounded-md bg-onboarding-background-200 border border-onboarding-border-100`}
|
||||
>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -124,18 +128,17 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
value={passwordFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`}
|
||||
disabled
|
||||
/>
|
||||
{passwordFormData.email.length > 0 && (
|
||||
<XCircle
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={handleEmailClear}
|
||||
/>
|
||||
<div className="flex-shrink-0 h-5 w-5 mr-2 bg-onboarding-background-200 hover:cursor-pointer">
|
||||
<XCircle className="h-5 w-5 stroke-custom-text-400" onClick={handleEmailClear} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input type="hidden" value={passwordFormData.email} name="email" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
|
||||
{mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"}
|
||||
@@ -147,7 +150,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
value={passwordFormData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
placeholder="Enter password"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
onFocus={() => setIsPasswordInputFocused(true)}
|
||||
onBlur={() => setIsPasswordInputFocused(false)}
|
||||
autoFocus
|
||||
@@ -166,6 +169,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
</div>
|
||||
{passwordSupport}
|
||||
</div>
|
||||
|
||||
{mode === EAuthModes.SIGN_UP && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
||||
@@ -178,7 +182,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
value={passwordFormData.confirm_password}
|
||||
onChange={(e) => 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"
|
||||
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
/>
|
||||
{showPassword?.retypePassword ? (
|
||||
<EyeOff
|
||||
@@ -197,19 +201,20 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2.5">
|
||||
{mode === EAuthModes.SIGN_IN ? (
|
||||
<>
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? (
|
||||
<Spinner height="20px" width="20px" />
|
||||
) : isSmtpConfigured ? (
|
||||
) : isSMTPConfigured ? (
|
||||
"Continue"
|
||||
) : (
|
||||
"Go to workspace"
|
||||
)}
|
||||
</Button>
|
||||
{instance && isSmtpConfigured && (
|
||||
{isSMTPConfigured && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={redirectToUniqueCodeSignIn}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CircleCheck, XCircle } from "lucide-react";
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
import { EAuthModes } from "@/helpers/authentication.helper";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
@@ -10,11 +9,14 @@ import useTimer from "@/hooks/use-timer";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
|
||||
type Props = {
|
||||
// services
|
||||
const authService = new AuthService();
|
||||
|
||||
type TAuthUniqueCodeForm = {
|
||||
mode: EAuthModes;
|
||||
email: string;
|
||||
handleEmailClear: () => void;
|
||||
submitButtonText: string;
|
||||
mode: EAuthModes;
|
||||
generateEmailUniqueCode: (email: string) => Promise<{ code: string } | undefined>;
|
||||
};
|
||||
|
||||
type TUniqueCodeFormValues = {
|
||||
@@ -27,55 +29,35 @@ const defaultValues: TUniqueCodeFormValues = {
|
||||
code: "",
|
||||
};
|
||||
|
||||
// services
|
||||
const authService = new AuthService();
|
||||
|
||||
export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
const { email, handleEmailClear, submitButtonText, mode } = props;
|
||||
export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
|
||||
const { mode, email, handleEmailClear, generateEmailUniqueCode } = props;
|
||||
// hooks
|
||||
// const { captureEvent } = useEventTracker();
|
||||
// derived values
|
||||
const defaultResetTimerValue = 5;
|
||||
// states
|
||||
const [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
|
||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// store hooks
|
||||
// const { captureEvent } = useEventTracker();
|
||||
// timer
|
||||
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
|
||||
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0);
|
||||
|
||||
const handleFormChange = (key: keyof TUniqueCodeFormValues, value: string) =>
|
||||
setUniqueCodeFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
const handleSendNewCode = async (email: string) => {
|
||||
const payload: IEmailCheckData = {
|
||||
email,
|
||||
};
|
||||
|
||||
await authService
|
||||
.generateUniqueCode(payload)
|
||||
.then(() => {
|
||||
setResendCodeTimer(30);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "A new unique code has been sent to your email.",
|
||||
});
|
||||
handleFormChange("code", "");
|
||||
})
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong while generating unique code. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRequestNewCode = async (email: string) => {
|
||||
setIsRequestingNewCode(true);
|
||||
|
||||
await handleSendNewCode(email)
|
||||
.then(() => setResendCodeTimer(30))
|
||||
.finally(() => setIsRequestingNewCode(false));
|
||||
const generateNewCode = async (email: string) => {
|
||||
try {
|
||||
setIsRequestingNewCode(true);
|
||||
const uniqueCode = await generateEmailUniqueCode(email);
|
||||
setResendCodeTimer(defaultResetTimerValue);
|
||||
handleFormChange("code", uniqueCode?.code || "");
|
||||
setIsRequestingNewCode(false);
|
||||
} catch {
|
||||
setResendCodeTimer(0);
|
||||
console.error("Error while requesting new code");
|
||||
setIsRequestingNewCode(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -83,11 +65,6 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
useEffect(() => {
|
||||
handleRequestNewCode(email);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
||||
const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting;
|
||||
|
||||
@@ -100,11 +77,14 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
onError={() => setIsSubmitting(false)}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<input type="hidden" value={uniqueCodeFormData.email} name="email" />
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||
<div
|
||||
className={`relative flex items-center rounded-md bg-onboarding-background-200 border border-onboarding-border-100`}
|
||||
>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -112,18 +92,17 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
value={uniqueCodeFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`}
|
||||
disabled
|
||||
/>
|
||||
{uniqueCodeFormData.email.length > 0 && (
|
||||
<XCircle
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={handleEmailClear}
|
||||
/>
|
||||
<div className="flex-shrink-0 h-5 w-5 mr-2 bg-onboarding-background-200 hover:cursor-pointer">
|
||||
<XCircle className="h-5 w-5 stroke-custom-text-400" onClick={handleEmailClear} />
|
||||
</div>
|
||||
)}
|
||||
<input type="hidden" value={uniqueCodeFormData.email} name="email" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="code">
|
||||
Unique code
|
||||
@@ -132,22 +111,18 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
name="code"
|
||||
value={uniqueCodeFormData.code}
|
||||
onChange={(e) => handleFormChange("code", e.target.value)}
|
||||
// FIXME:
|
||||
// hasError={Boolean(errors.code)}
|
||||
placeholder="gets-sets-flys"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
{/* )}
|
||||
/> */}
|
||||
<div className="flex w-full items-center justify-between px-1 text-xs">
|
||||
<div className="flex w-full items-center justify-between px-1 text-xs pt-1">
|
||||
<p className="flex items-center gap-1 font-medium text-green-700">
|
||||
<CircleCheck height={12} width={12} />
|
||||
Paste the code sent to your email
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRequestNewCode(uniqueCodeFormData.email)}
|
||||
onClick={() => generateNewCode(uniqueCodeFormData.email)}
|
||||
className={`${
|
||||
isRequestNewCodeDisabled
|
||||
? "text-onboarding-text-400"
|
||||
@@ -163,15 +138,12 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isRequestingNewCode ? (
|
||||
"Sending code"
|
||||
) : isSubmitting ? (
|
||||
<Spinner height="20px" width="20px" />
|
||||
) : (
|
||||
submitButtonText
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isRequestingNewCode ? "Sending code" : isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,9 +24,9 @@ export const PasswordStrengthMeter: React.FC<Props> = (props: Props) => {
|
||||
text = "Password is too short";
|
||||
textColor = `text-[#DC3E42]`;
|
||||
} else if (strength < 3) {
|
||||
bars = [`bg-[#FFBA18]`, `bg-[#FFBA18]`, `bg-[#F0F0F3]`];
|
||||
bars = [`bg-[#DC3E42]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
|
||||
text = "Password is weak";
|
||||
textColor = `text-[#FFBA18]`;
|
||||
textColor = `text-[#F0F0F3]`;
|
||||
} else {
|
||||
bars = [`bg-[#3E9B4F]`, `bg-[#3E9B4F]`, `bg-[#3E9B4F]`];
|
||||
text = "Password is strong";
|
||||
|
||||
@@ -73,7 +73,7 @@ const ButtonContent: React.FC<ButtonContentProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
{showCount ? (
|
||||
<div className="relative flex max-w-full items-center gap-1">
|
||||
<div className="relative flex items-center max-w-full gap-1">
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{(value.length > 0 || !!placeholder) && (
|
||||
<div className="max-w-40 flex-grow truncate">
|
||||
|
||||
@@ -15,18 +15,16 @@ type Props = {
|
||||
handleChartView: (view: TGanttViews) => void;
|
||||
handleToday: () => void;
|
||||
loaderTitle: string;
|
||||
title: string;
|
||||
toggleFullScreenMode: () => void;
|
||||
};
|
||||
|
||||
export const GanttChartHeader: React.FC<Props> = observer((props) => {
|
||||
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props;
|
||||
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode } = props;
|
||||
// chart hook
|
||||
const { currentView } = useGanttChart();
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2">
|
||||
<div className="flex items-center gap-2 text-lg font-medium">{title}</div>
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto text-sm font-medium">{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}</div>
|
||||
</div>
|
||||
|
||||
@@ -172,7 +172,6 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
||||
handleToday={handleToday}
|
||||
loaderTitle={loaderTitle}
|
||||
title={title}
|
||||
/>
|
||||
<GanttChartMainContent
|
||||
blocks={blocks}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Plus } from "lucide-react";
|
||||
import { Breadcrumbs, Button, DiceIcon } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { ModuleViewHeader } from "@/components/modules";
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
@@ -57,6 +58,7 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ModuleViewHeader />
|
||||
{canUserCreateModule && (
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// helpers
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
import { ViewListHeader } from "@/components/views";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// constants
|
||||
import { useCommandPalette, useProject, useUser } from "@/hooks/store";
|
||||
@@ -58,6 +59,7 @@ export const ProjectViewsHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
{canUserCreateIssue && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<ViewListHeader />
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@plane/ui";
|
||||
// helpers
|
||||
import { ADMIN_BASE_URL, ADMIN_BASE_PATH } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
// import { useInstance } from "@/hooks/store";
|
||||
// images
|
||||
import PlaneTakeOffImage from "@/public/plane-takeoff.png";
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||
|
||||
export const InstanceNotReady: FC = observer(() => {
|
||||
// hooks
|
||||
// const { instance } = useInstance();
|
||||
|
||||
const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH + "setup/?auth_enabled=0");
|
||||
export const InstanceNotReady: FC = () => {
|
||||
const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH + "/setup/?auth_enabled=0");
|
||||
|
||||
return (
|
||||
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
|
||||
@@ -48,4 +42,4 @@ export const InstanceNotReady: FC = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
isParentIssueModalOpen,
|
||||
toggleParentIssueModal,
|
||||
removeSubIssue,
|
||||
subIssues: { setSubIssueHelpers },
|
||||
subIssues: { setSubIssueHelpers, fetchSubIssues },
|
||||
} = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
@@ -47,7 +47,8 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
try {
|
||||
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
||||
await issueOperations.fetch(workspaceSlug, projectId, issueId);
|
||||
toggleParentIssueModal(issueId);
|
||||
_issueId && (await fetchSubIssues(workspaceSlug, projectId, _issueId));
|
||||
toggleParentIssueModal(null);
|
||||
} catch (error) {
|
||||
console.error("something went wrong while fetching the issue");
|
||||
}
|
||||
@@ -62,6 +63,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
await fetchSubIssues(workspaceSlug, projectId, parentIssueId);
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
|
||||
@@ -57,11 +57,14 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
||||
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||
|
||||
const issue = issuesMap[issueId];
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
const { isMobile } = usePlatformOS();
|
||||
if (!issue) return null;
|
||||
|
||||
const canEditIssueProperties = canEditProperties(issue.project_id);
|
||||
const projectIdentifier = getProjectIdentifierById(issue.project_id);
|
||||
// if sub issues have been fetched for the issue, use that for count or use issue's sub_issues_count
|
||||
const subIssuesCount = subIssues ? subIssues.length : issue.sub_issues_count;
|
||||
|
||||
const paddingLeft = `${spacingLeft}px`;
|
||||
|
||||
@@ -90,11 +93,11 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full truncate" style={issue.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}>
|
||||
<div className="flex w-full truncate" style={nestingLevel !== 0 ? { paddingLeft } : {}}>
|
||||
<div className="flex flex-grow items-center gap-3 truncate">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
{issue.sub_issues_count > 0 && (
|
||||
{subIssuesCount > 0 && (
|
||||
<button
|
||||
className="flex items-center justify-center h-5 w-5 cursor-pointer rounded-sm text-custom-text-400 hover:text-custom-text-300"
|
||||
onClick={handleToggleExpand}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
|
||||
placeholder="Select cycle"
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonContainerClassName="w-full relative flex items-center p-2"
|
||||
buttonClassName="relative border-[0.5px] border-custom-border-400 h-4.5"
|
||||
buttonClassName="relative leading-4 h-4.5 bg-transparent"
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
|
||||
placeholder="Select modules"
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonContainerClassName="w-full relative flex items-center p-2"
|
||||
buttonClassName="relative border-[0.5px] border-custom-border-400 h-4.5"
|
||||
buttonClassName="relative leading-4 h-4.5 bg-transparent"
|
||||
onClose={onClose}
|
||||
multiple
|
||||
showCount
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./columns";
|
||||
export * from "./roots";
|
||||
export * from "./spreadsheet-view";
|
||||
export * from "./quick-add-issue-form";
|
||||
export * from "./spreadsheet-header-column";
|
||||
|
||||
@@ -50,7 +50,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
containerRef,
|
||||
issueIds,
|
||||
spreadsheetColumnsList,
|
||||
spacingLeft = 14,
|
||||
spacingLeft = 6,
|
||||
} = props;
|
||||
|
||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||
@@ -96,7 +96,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
nestingLevel={nestingLevel + 1}
|
||||
spacingLeft={spacingLeft + (displayProperties.key ? 16 : 28)}
|
||||
spacingLeft={spacingLeft + (displayProperties.key ? 12 : 28)}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
updateIssue={updateIssue}
|
||||
portalElement={portalElement}
|
||||
@@ -140,7 +140,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
isExpanded,
|
||||
setExpanded,
|
||||
spreadsheetColumnsList,
|
||||
spacingLeft = 14,
|
||||
spacingLeft = 6,
|
||||
} = props;
|
||||
// states
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
@@ -166,6 +166,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||
|
||||
const issueDetail = issue.getIssueById(issueId);
|
||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||
|
||||
const paddingLeft = `${spacingLeft}px`;
|
||||
|
||||
@@ -199,6 +200,8 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
};
|
||||
|
||||
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
||||
// if sub issues have been fetched for the issue, use that for count or use issue's sub_issues_count
|
||||
const subIssuesCount = subIssues ? subIssues.length : issueDetail.sub_issues_count;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -218,18 +221,22 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
disabled={!!issueDetail?.tempId}
|
||||
>
|
||||
<div
|
||||
className="flex min-w-min items-center gap-1 px-4 py-2.5 pr-0"
|
||||
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
|
||||
className="flex min-w-min items-center gap-0.5 px-4 py-2.5 pl-1.5 pr-0"
|
||||
style={nestingLevel !== 0 ? { paddingLeft } : {}}
|
||||
>
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
{issueDetail.sub_issues_count > 0 && (
|
||||
<button
|
||||
className="flex items-center justify-center h-5 w-5 cursor-pointer rounded-sm text-custom-text-400 hover:text-custom-text-300"
|
||||
onClick={handleToggleExpand}
|
||||
>
|
||||
<ChevronRight className={`h-4 w-4 ${isExpanded ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
{/* bulk ops */}
|
||||
<span className="size-3.5" />
|
||||
<div className="flex size-4 items-center justify-center">
|
||||
{subIssuesCount > 0 && (
|
||||
<button
|
||||
className="flex items-center justify-center size-4 cursor-pointer rounded-sm text-custom-text-400 hover:text-custom-text-300"
|
||||
onClick={handleToggleExpand}
|
||||
>
|
||||
<ChevronRight className={`size-4 ${isExpanded ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// ui
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
// types
|
||||
import { LayersIcon } from "@plane/ui";
|
||||
// constants
|
||||
// components
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
import { SpreadsheetHeaderColumn } from "./spreadsheet-header-column";
|
||||
import { SpreadsheetHeaderColumn } from "@/components/issues/issue-layouts";
|
||||
|
||||
interface Props {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
@@ -25,13 +24,8 @@ export const SpreadsheetHeader = (props: Props) => {
|
||||
className="sticky left-0 z-[15] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||
<span className="flex h-full w-24 flex-shrink-0 items-center px-4 py-2.5">
|
||||
<span className="mr-1.5 text-custom-text-400">#</span>ID
|
||||
</span>
|
||||
</WithDisplayPropertiesHOC>
|
||||
<span className="flex h-full w-full flex-grow items-center justify-center px-4 py-2.5">
|
||||
<LayersIcon className="mr-1.5 h-4 w-4 text-custom-text-400" />
|
||||
<span className="flex h-full w-full flex-grow items-center pl-5 px-4 py-2.5">
|
||||
<LayersIcon className="mr-1 h-4 w-4 text-custom-text-400" />
|
||||
Issue
|
||||
</span>
|
||||
</th>
|
||||
|
||||
@@ -2,11 +2,12 @@ import React, { useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Info } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, Info, MoveRight, User2 } from "lucide-react";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, LayersIcon, Tooltip, setPromiseToast } from "@plane/ui";
|
||||
import { LayersIcon, Tooltip, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
import { FavoriteStar } from "@/components/core";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
// constants
|
||||
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
|
||||
@@ -145,6 +146,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
: `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
|
||||
: "0 Issue";
|
||||
|
||||
const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Link ref={parentRef} href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}>
|
||||
@@ -179,16 +182,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||
<span className="text-xs text-custom-text-300">{issueCount ?? "0 Issue"}</span>
|
||||
</div>
|
||||
{moduleDetails.member_ids?.length > 0 && (
|
||||
<Tooltip tooltipContent={`${moduleDetails.member_ids.length} Members`} isMobile={isMobile}>
|
||||
<div className="flex cursor-default items-center gap-1">
|
||||
<AvatarGroup showTooltip={false}>
|
||||
{moduleDetails.member_ids.map((member_id) => {
|
||||
const member = getUserDetails(member_id);
|
||||
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
{moduleLeadDetails ? (
|
||||
<span className="cursor-default">
|
||||
<ButtonAvatars showTooltip={false} userIds={moduleLeadDetails?.id} />
|
||||
</span>
|
||||
) : (
|
||||
<Tooltip tooltipContent="No lead">
|
||||
<span className="cursor-default flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@@ -217,11 +219,13 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex items-center justify-between py-0.5">
|
||||
{isDateValid ? (
|
||||
<>
|
||||
<span className="text-xs text-custom-text-300">
|
||||
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
|
||||
</span>
|
||||
</>
|
||||
<div className="h-6 flex items-center gap-1.5 text-custom-text-300 border-[0.5px] border-custom-border-300 rounded text-xs px-2 cursor-default">
|
||||
<CalendarClock className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="flex-grow truncate">{renderFormattedDate(startDate)}</span>
|
||||
<MoveRight className="h-3 w-3 flex-shrink-0" />
|
||||
<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="flex-grow truncate">{renderFormattedDate(endDate)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-custom-text-400">No due date</span>
|
||||
)}
|
||||
@@ -229,7 +233,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="absolute right-4 bottom-3.5 flex items-center gap-1.5">
|
||||
<div className="absolute right-4 bottom-[18px] flex items-center gap-1.5">
|
||||
{isEditingAllowed && (
|
||||
<FavoriteStar
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -2,11 +2,11 @@ import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { User2 } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, MoveRight, User2 } from "lucide-react";
|
||||
// types
|
||||
import { IModule } from "@plane/types";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, Tooltip, setPromiseToast } from "@plane/ui";
|
||||
import { Tooltip, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
import { FavoriteStar } from "@/components/core";
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
@@ -18,7 +18,7 @@ import { EUserProjectRoles } from "@/constants/project";
|
||||
import { getDate, renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useModule, useUser } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { ButtonAvatars } from "../dropdowns/member/avatar";
|
||||
|
||||
type Props = {
|
||||
moduleId: string;
|
||||
@@ -38,7 +38,6 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
|
||||
const { addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||
const { getUserDetails } = useMember();
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
// derived values
|
||||
const endDate = getDate(moduleDetails.target_date);
|
||||
@@ -109,11 +108,23 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderDate && (
|
||||
<div className="h-6 flex items-center gap-1.5 text-custom-text-300 border-[0.5px] border-custom-border-300 rounded text-xs px-2 cursor-default">
|
||||
<CalendarClock className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="flex-grow truncate">{renderFormattedDate(startDate)}</span>
|
||||
<MoveRight className="h-3 w-3 flex-shrink-0" />
|
||||
<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="flex-grow truncate">{renderFormattedDate(endDate)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{moduleStatus && (
|
||||
<span
|
||||
className="flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
|
||||
className="flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs cursor-default"
|
||||
style={{
|
||||
color: moduleStatus.color,
|
||||
backgroundColor: `${moduleStatus.color}20`,
|
||||
@@ -123,29 +134,18 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{renderDate && (
|
||||
<span className=" text-xs text-custom-text-300">
|
||||
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
|
||||
{moduleLeadDetails ? (
|
||||
<span className="cursor-default">
|
||||
<ButtonAvatars showTooltip={false} userIds={moduleLeadDetails?.id} />
|
||||
</span>
|
||||
) : (
|
||||
<Tooltip tooltipContent="No lead">
|
||||
<span className="cursor-default flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip tooltipContent={`${moduleDetails?.member_ids?.length || 0} Members`} isMobile={isMobile}>
|
||||
<div className="flex w-10 cursor-default items-center justify-center gap-1">
|
||||
{moduleDetails.member_ids.length > 0 ? (
|
||||
<AvatarGroup showTooltip={false}>
|
||||
{moduleDetails.member_ids.map((member_id) => {
|
||||
const member = getUserDetails(member_id);
|
||||
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
|
||||
})}
|
||||
</AvatarGroup>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{isEditingAllowed && !moduleDetails.archived_at && (
|
||||
<FavoriteStar
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -77,7 +77,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
) : progress === 100 ? (
|
||||
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
|
||||
) : (
|
||||
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
|
||||
<span className="text-[9px] text-custom-text-300">{`${progress}%`}</span>
|
||||
)}
|
||||
</CircularProgressIndicator>
|
||||
}
|
||||
@@ -89,9 +89,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
<Info className="h-4 w-4 text-custom-text-400" />
|
||||
</button>
|
||||
}
|
||||
actionableItems={
|
||||
<ModuleListItemAction moduleId={moduleId} moduleDetails={moduleDetails} parentRef={parentRef} />
|
||||
}
|
||||
actionableItems={<ModuleListItemAction moduleId={moduleId} moduleDetails={moduleDetails} parentRef={parentRef} />}
|
||||
isMobile={isMobile}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
|
||||
@@ -173,7 +173,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
<div className="w-full space-y-2.5 overflow-hidden">
|
||||
<div className="flex items-start">
|
||||
{!notification.message ? (
|
||||
<div className="w-full break-words text-sm">
|
||||
<div className="w-full break-all text-sm group-hover:pr-24 line-clamp-2">
|
||||
<span className="font-semibold">
|
||||
{notificationTriggeredBy.is_bot
|
||||
? notificationTriggeredBy.first_name
|
||||
|
||||
@@ -148,7 +148,10 @@ export const CreateWorkspace: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<form className="w-full mx-auto mt-2 space-y-4" onSubmit={handleSubmit(handleCreateWorkspace)}>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="name">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="name"
|
||||
>
|
||||
Workspace name
|
||||
</label>
|
||||
<Controller
|
||||
@@ -187,7 +190,10 @@ export const CreateWorkspace: React.FC<Props> = (props) => {
|
||||
{errors.name && <span className="text-sm text-red-500">{errors.name.message}</span>}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="slug">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="slug"
|
||||
>
|
||||
Workspace URL
|
||||
</label>
|
||||
<Controller
|
||||
@@ -224,7 +230,10 @@ export const CreateWorkspace: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<hr className="w-full border-onboarding-border-100" />
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="organization_size">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="organization_size"
|
||||
>
|
||||
Company size
|
||||
</label>
|
||||
<div className="w-full">
|
||||
|
||||
@@ -157,6 +157,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = (props) => {
|
||||
hasError={Boolean(errors.emails?.[index]?.email)}
|
||||
placeholder={placeholderEmails[index % placeholderEmails.length]}
|
||||
className="w-full border-onboarding-border-100 text-xs placeholder:text-onboarding-text-400 sm:text-sm"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -333,7 +333,10 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="first_name">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="first_name"
|
||||
>
|
||||
First name
|
||||
</label>
|
||||
<Controller
|
||||
@@ -364,7 +367,10 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
||||
{errors.first_name && <span className="text-sm text-red-500">{errors.first_name.message}</span>}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="last_name">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="last_name"
|
||||
>
|
||||
Last name
|
||||
</label>
|
||||
<Controller
|
||||
@@ -485,7 +491,10 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
||||
{profileSetupStep !== EProfileSetupSteps.USER_DETAILS && (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="role">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="role"
|
||||
>
|
||||
What role are you working on? Choose one.
|
||||
</label>
|
||||
<Controller
|
||||
@@ -513,7 +522,10 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
||||
{errors.role && <span className="text-sm text-red-500">{errors.role.message}</span>}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="use_case">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="use_case"
|
||||
>
|
||||
What is your domain expertise? Choose one.
|
||||
</label>
|
||||
<Controller
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user